diff --git a/README.md b/README.md index 9c68e6a9..3d351292 100644 --- a/README.md +++ b/README.md @@ -25,50 +25,67 @@ How often do you update a work item? How often do you create a new security grou - v3 (VS 2015+ / NuGet 3.x): `https://www.myget.org/F/qwiq/api/v3/index.json` - v2 (VS 2013 / NuGet 2.x): `https://www.myget.org/F/qwiq/api/v2` -Once the feed is configured, instald via the nuget UI (as [Microsoft.Qwiq.Core](https://www.myget.org/feed/qwiq/package/nuget/Microsoft.Qwiq.Core)), or via the nuget package manager console: +Once the feed is configured, install via the nuget UI or via the nuget package manager console ### Install Core +From the NuGet package manager console ``` PM> Install-Package Microsoft.Qwiq.Core ``` +Or via the UI [Microsoft.Qwiq.Core](https://www.myget.org/feed/qwiq/package/nuget/Microsoft.Qwiq.Core), + + +### Install Client +We now have two clients: one for SOAP, and one for REST + +From the NuGet package manager console +``` +PM> Install-Package Microsoft.Qwiq.Client.Soap +``` +Or via the UI [Microsoft.Qwiq.Client.Soap](https://www.myget.org/feed/qwiq/package/nuget/Microsoft.Qwiq.Client.Soap), ### Basic Usage ```csharp using Microsoft.Qwiq; using Microsoft.Qwiq.Credentials; + +using Microsoft.VisualStudio.Services.Client; ... -// Create credentials objects based on the current process and OS identity // We support -// - Username and password -// - PAT -// - Federated Windows credentials -var creds = CredentialsFactory.CreateCredentials(); -var uri = new Uri("[Tfs Tenant Uri]"); +// - OAuth2 +// - Personal Access Token (PAT) +// - Username and password (BASIC) +// - Windows credentials (NTLM or Federated with Azure Active Directory) +// - Anonymous +// Use the full URI, including the collection. Example: https://QWIQ.VisualStudio.com/DefaultCollection +var uri = new Uri("[Tfs Tenant Uri]"); +var options = new AuthenticationOptions(uri, AuthenticationType.Windows); var store = WorkItemStoreFactory - .GetInstance() - .Create(uri, creds); -// ^^^ store and re-use this! + .Default + .Create(options); // Execute WIQL var items = store.Query(@" SELECT [System.Id] FROM WorkItems - WHERE WorkItemType = 'Bug' AND State = 'Active'"); + WHERE [System.WorkItemType] = 'Bug' AND State = 'Active'"); ``` ```powershell [Reflection.Assembly]::LoadFrom("E:\Path\To\Microsoft.Qwiq.Core.dll") +# Can use SOAP or REST clients here +[Reflection.Assembly]::LoadFrom("E:\Path\To\Microsoft.Qwiq.Client.Soap.dll") -$creds = [Microsoft.Qwiq.Credentials.CredentialsFactory]::CreateCredentials() $uri = [Uri]"[Tfs Tenant Uri]" -$store = [Microsoft.Qwiq.WorkItemStoreFactory]::GetInstance().Create($uri, $creds) +$options = New-Object Microsoft.Qwiq.Credentials.AuthenticationOptions $uri,Windows +$store = [Microsoft.Qwiq.Client.Soap.WorkItemStoreFactory]::Default.Create($options) $items = $store.Query(@" SELECT [System.Id] FROM WorkItems - WHERE WorkItemType = 'Bug' AND State = 'Active'") + WHERE [System.WorkItemType] = 'Bug' AND State = 'Active'", $false) ``` ## Contributing diff --git a/src/Qwiq.Core.Rest/FieldDefinitionCollection.cs b/src/Qwiq.Core.Rest/FieldDefinitionCollection.cs index cab6efc4..84d79dd0 100644 --- a/src/Qwiq.Core.Rest/FieldDefinitionCollection.cs +++ b/src/Qwiq.Core.Rest/FieldDefinitionCollection.cs @@ -25,7 +25,7 @@ internal FieldDefinitionCollection(IEnumerable typeField } private FieldDefinitionCollection(List fieldDefinitions) - : base(fieldDefinitions) + : base(fieldDefinitions.Distinct().ToList()) { } } diff --git a/src/Qwiq.Core.Rest/LevelOrderEnumerator.cs b/src/Qwiq.Core.Rest/LevelOrderEnumerator.cs new file mode 100644 index 00000000..e9191abd --- /dev/null +++ b/src/Qwiq.Core.Rest/LevelOrderEnumerator.cs @@ -0,0 +1,85 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; + +namespace Microsoft.Qwiq.Client.Rest +{ + internal class LevelOrderEnumerator : IEnumerator + { + private readonly Queue _queue; + private int _currentGenerationCount; + private int _nextGenerationCount; + private readonly WorkItemClassificationNode _root; + private readonly int? _maxDepth; + private int _currentDepth; + + public LevelOrderEnumerator(WorkItemClassificationNode root, int? maxDepth = null) + { + _root = root; + _maxDepth = maxDepth; + _currentDepth = 0; + _queue = new Queue(); + _currentGenerationCount = 1; + _nextGenerationCount = 0; + Current = null; + } + + public void Dispose() + { + } + + public bool MoveNext() + { + if (Current == null) + { + Current = _root; + ProcessCurrent(); + + return true; + } + + if (_queue.Count == 0) + { + return false; + } + + if (_currentGenerationCount == 0) + { + _currentDepth++; + _currentGenerationCount = _nextGenerationCount; + _nextGenerationCount = 0; + } + + Current = _queue.Dequeue(); + ProcessCurrent(); + + return true; + } + + private void ProcessCurrent() + { + _currentGenerationCount--; + if (_currentDepth >= _maxDepth) return; + + Debug.Assert(Current != null, nameof(Current) + " != null"); + + if (Current.Children == null) return; + + foreach (var child in Current.Children) + { + _nextGenerationCount++; + _queue.Enqueue(child); + } + } + + public void Reset() + { + Current = null; + } + + public WorkItemClassificationNode Current { get; private set; } + + object IEnumerator.Current => Current; + } +} \ No newline at end of file diff --git a/src/Qwiq.Core.Rest/Node.cs b/src/Qwiq.Core.Rest/Node.cs deleted file mode 100644 index a774ef30..00000000 --- a/src/Qwiq.Core.Rest/Node.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Diagnostics.Contracts; -using System.Linq; - -using JetBrains.Annotations; - -using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; - -namespace Microsoft.Qwiq.Client.Rest -{ - internal class Node : Qwiq.Node - { - internal Node([NotNull] WorkItemClassificationNode node) - : this(node, null) - { - } - - internal Node([NotNull] WorkItemClassificationNode node, [CanBeNull] INode parentNode) - : base( - node.Id, - node.StructureType == TreeNodeStructureType.Area, - node.StructureType == TreeNodeStructureType.Iteration, - node.Name, - new Uri(node.Url), - () => parentNode, - n => node.Children?.Any() ?? false ? node.Children.Select(s => new Node(s, n)).ToList() : Enumerable.Empty()) - { - Contract.Requires(node != null); - - if (node == null) throw new ArgumentNullException(nameof(node)); - } - } -} \ No newline at end of file diff --git a/src/Qwiq.Core.Rest/Project.cs b/src/Qwiq.Core.Rest/Project.cs index e3a649c2..d1fcb432 100644 --- a/src/Qwiq.Core.Rest/Project.cs +++ b/src/Qwiq.Core.Rest/Project.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; - using JetBrains.Annotations; using Microsoft.TeamFoundation.Core.WebApi; @@ -33,38 +32,9 @@ internal Project([NotNull] TeamProjectReference project, [NotNull] WorkItemStore return new WorkItemTypeCollection(wits2); }), - new Lazy( - () => - { - var result = store.NativeWorkItemStore - .Value - .GetClassificationNodeAsync( - project.Id, - TreeStructureGroup.Areas, - null, - int.MaxValue) - .GetAwaiter() - .GetResult(); - - // SOAP Client does not return just the root, so return the root's children to match implementation - var n = new Node(result).ChildNodes; - return n; - }), - new Lazy( - () => - { - var result = store.NativeWorkItemStore - .Value - .GetClassificationNodeAsync( - project.Name, - TreeStructureGroup.Iterations, - null, - int.MaxValue) - .GetAwaiter() - .GetResult(); - - return new Node(result).ChildNodes; - })) + new Lazy>(() => WorkItemClassificationNodeCollectionBuilder.BuildAsync(store.NativeWorkItemStore.Value.GetClassificationNodeAsync(project.Name, TreeStructureGroup.Areas, null, int.MaxValue)).GetAwaiter().GetResult()), + new Lazy>(() => WorkItemClassificationNodeCollectionBuilder.BuildAsync(store.NativeWorkItemStore.Value.GetClassificationNodeAsync(project.Name, TreeStructureGroup.Iterations, null, int.MaxValue)).GetAwaiter().GetResult()) + ) { } } diff --git a/src/Qwiq.Core.Rest/Qwiq.Client.Rest.csproj b/src/Qwiq.Core.Rest/Qwiq.Client.Rest.csproj index ff0a23f2..75fbea84 100644 --- a/src/Qwiq.Core.Rest/Qwiq.Client.Rest.csproj +++ b/src/Qwiq.Core.Rest/Qwiq.Client.Rest.csproj @@ -1,6 +1,6 @@  - + @@ -84,8 +84,8 @@ + - @@ -94,6 +94,7 @@ + @@ -118,8 +119,8 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + \ No newline at end of file diff --git a/src/Qwiq.Core.Rest/TeamFoundationIdentity.cs b/src/Qwiq.Core.Rest/TeamFoundationIdentity.cs index 18762a1f..eb934315 100644 --- a/src/Qwiq.Core.Rest/TeamFoundationIdentity.cs +++ b/src/Qwiq.Core.Rest/TeamFoundationIdentity.cs @@ -1,12 +1,10 @@ -using System; +using JetBrains.Annotations; +using Microsoft.VisualStudio.Services.Identity; +using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; -using JetBrains.Annotations; - -using Microsoft.VisualStudio.Services.Identity; - namespace Microsoft.Qwiq.Client.Rest { internal class TeamFoundationIdentity : Qwiq.TeamFoundationIdentity @@ -15,35 +13,28 @@ internal class TeamFoundationIdentity : Qwiq.TeamFoundationIdentity private readonly Identity _identity; - private readonly Lazy> _memberOf; - - private readonly Lazy> _members; - internal TeamFoundationIdentity([NotNull] Identity identity) - : base(identity.IsActive, identity.Id, identity.UniqueUserId) + : base( + identity.IsActive, + identity.Id, + identity.UniqueUserId, + identity.MemberOf.Select(item => item.AsProxy()).ToArray(), + identity.Members.Select(item => item.AsProxy()).ToArray()) { Contract.Requires(identity != null); - + _identity = identity ?? throw new ArgumentNullException(nameof(identity)); DisplayName = identity.DisplayName; - IsContainer = identity.IsContainer; - _descriptor = new Lazy(() => identity.Descriptor.AsProxy()); - _memberOf = new Lazy>(() => identity.MemberOf.Select(item => item.AsProxy())); - _members = new Lazy>(() => identity.Members.Select(item => item.AsProxy())); } - + public override IIdentityDescriptor Descriptor => _descriptor.Value; public override string DisplayName { get; } public override bool IsContainer { get; } - public override IEnumerable MemberOf => _memberOf.Value; - - public override IEnumerable Members => _members.Value; - public override string GetAttribute(string name, string defaultValue) { if (_identity.Properties.TryGetValue(name, out object obj)) return obj?.ToString() ?? defaultValue; diff --git a/src/Qwiq.Core.Rest/TfsConnectionFactory.cs b/src/Qwiq.Core.Rest/TfsConnectionFactory.cs index b8c3a8a7..74dbd8a7 100644 --- a/src/Qwiq.Core.Rest/TfsConnectionFactory.cs +++ b/src/Qwiq.Core.Rest/TfsConnectionFactory.cs @@ -1,7 +1,6 @@ -using System; - using Microsoft.VisualStudio.Services.Common; using Microsoft.VisualStudio.Services.WebApi; +using System; namespace Microsoft.Qwiq.Client.Rest { @@ -20,8 +19,6 @@ protected override ITeamProjectCollection ConnectToTfsCollection(Uri endpoint, V tfsServer.Settings.BypassProxyOnLocal = true; tfsServer.Settings.CompressionEnabled = true; - - tfsServer.ConnectAsync(VssConnectMode.Automatic).GetAwaiter().GetResult(); if (!tfsServer.HasAuthenticated) throw new InvalidOperationException("Could not connect."); return tfsServer.AsProxy(); @@ -29,7 +26,7 @@ protected override ITeamProjectCollection ConnectToTfsCollection(Uri endpoint, V // ReSharper disable ClassNeverInstantiated.Local private class Nested - // ReSharper restore ClassNeverInstantiated.Local + // ReSharper restore ClassNeverInstantiated.Local { // ReSharper disable MemberHidesStaticFromOuterClass internal static readonly TfsConnectionFactory Instance = new TfsConnectionFactory(); diff --git a/src/Qwiq.Core.Rest/WorkItem.cs b/src/Qwiq.Core.Rest/WorkItem.cs index 3a5b2e11..7c4f037b 100644 --- a/src/Qwiq.Core.Rest/WorkItem.cs +++ b/src/Qwiq.Core.Rest/WorkItem.cs @@ -108,12 +108,6 @@ public override int HyperlinkCount public override int Id => _item.Id.GetValueOrDefault(0); - public override string Keywords - { - get => GetValue(WorkItemFields.Keywords); - set => SetValue(WorkItemFields.Keywords, value); - } - public override ICollection Links => _links ?? (_links = new LinkCollection((List)_item.Relations, _linkFunc)); @@ -135,8 +129,6 @@ public override int RelatedLinkCount public override int Rev => _item.Rev.GetValueOrDefault(0); - public override Uri Uri => _uri ?? (_uri = new Uri(_item.Url, UriKind.Absolute)); - public override string Url { get; } protected override object GetValue(string name) diff --git a/src/Qwiq.Core.Rest/WorkItemClassificationNodeCollectionBuilder.cs b/src/Qwiq.Core.Rest/WorkItemClassificationNodeCollectionBuilder.cs new file mode 100644 index 00000000..45961f33 --- /dev/null +++ b/src/Qwiq.Core.Rest/WorkItemClassificationNodeCollectionBuilder.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; + +namespace Microsoft.Qwiq.Client.Rest +{ + internal static class WorkItemClassificationNodeCollectionBuilder + { + private static readonly Regex AreaRegex = new Regex("/classificationNodes/Areas/(?.*)", RegexOptions.Compiled | RegexOptions.Singleline); + private static readonly Regex IterationRegex = new Regex("/classificationNodes/Iterations/(?.*)", RegexOptions.Compiled | RegexOptions.Singleline); + + public static async Task> BuildAsync(Task collection) + { + var n = await collection.ConfigureAwait(false); + + // SOAP client does not return the root (e.g. "\"), so return the root's children to match implementation + return new WorkItemClassificationNodeCollection(NewMethod(n.Children, n.Name)); + } + + private static IEnumerable> NewMethod(IEnumerable collection, string rootPath) + { + foreach (var n in collection) + { + var e = new LevelOrderEnumerator(n); + while (e.MoveNext()) + { + Debug.Assert(e.Current != null, "e.Current != null"); + + var t = e.Current.StructureType == TreeNodeStructureType.Area + ? NodeType.Area : e.Current.StructureType == TreeNodeStructureType.Iteration ? NodeType.Iteration : NodeType.None; + + var u = DecodeUrlString(e.Current.Url); + + var m = (t == NodeType.Area ? AreaRegex : IterationRegex).Match(u); + var p = m.Groups["path"].Value; + p = rootPath + "\\" + p.Replace("%20", " ").Replace("/", "\\"); + + yield return new WorkItemClassificationNode( + e.Current.Id, + t, + p, + new Uri(e.Current.Url) + ); + } + } + } + + private static string DecodeUrlString(string url) + { + string newUrl; + while ((newUrl = Uri.UnescapeDataString(url)) != url) + url = newUrl; + return newUrl; + } + } +} \ No newline at end of file diff --git a/src/Qwiq.Core.Rest/WorkItemStore.cs b/src/Qwiq.Core.Rest/WorkItemStore.cs index c9f89dbb..8bff2465 100644 --- a/src/Qwiq.Core.Rest/WorkItemStore.cs +++ b/src/Qwiq.Core.Rest/WorkItemStore.cs @@ -1,12 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; - -using Microsoft.TeamFoundation.Core.WebApi; +using Microsoft.TeamFoundation.Core.WebApi; using Microsoft.TeamFoundation.WorkItemTracking.WebApi; using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; using Microsoft.VisualStudio.Services.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; namespace Microsoft.Qwiq.Client.Rest { @@ -51,13 +50,11 @@ internal WorkItemStore( /// Qwiq.WorkItemStoreConfiguration IWorkItemStore.Configuration => Configuration; - public WorkItemStoreConfiguration Configuration { get;} + public WorkItemStoreConfiguration Configuration { get; } public IFieldDefinitionCollection FieldDefinitions => _fieldDefinitions ?? (_fieldDefinitions = new FieldDefinitionCollection(this)); - - public IProjectCollection Projects => _projects ?? (_projects = ProjectCollectionFactory()); public IRegisteredLinkTypeCollection RegisteredLinkTypes { get; } @@ -194,9 +191,28 @@ private void Dispose(bool disposing) private IProjectCollection ProjectCollectionFactory() { + const string continuationHeader = "x-ms-continuationtoken"; + + // See https://www.visualstudio.com/en-us/docs/integrate/api/tfs/projects + const ProjectState defaultStateFilter = ProjectState.WellFormed; + const int defaultNumberOfTeamProjects = 100; + + // Use the page size configured by the client if it is higher than the default + // Otherwise, with a default of 50, we would need to make multiple trips to load all the data + var pageSize = Math.Max(defaultNumberOfTeamProjects, Configuration.PageSize); + using (var projectHttpClient = _tfs.Value.GetClient()) { - var projects = (List)projectHttpClient.GetProjects(ProjectState.WellFormed).GetAwaiter().GetResult(); + var projects = new List(pageSize); + + var projectReferences = projectHttpClient.GetProjects(defaultStateFilter, pageSize).GetAwaiter().GetResult(); + projects.AddRange(projectReferences); + while (projectHttpClient.LastResponseContext.Headers.Contains(continuationHeader)) + { + projectReferences = projectHttpClient.GetProjects(defaultStateFilter, pageSize, projects.Count).GetAwaiter().GetResult(); + projects.AddRange(projectReferences); + } + var projects2 = new List(projects.Count + 1); for (var i = 0; i < projects.Count; i++) diff --git a/src/Qwiq.Core.Rest/WorkItemStoreConfiguration.cs b/src/Qwiq.Core.Rest/WorkItemStoreConfiguration.cs index 2def4d35..5ffc7e96 100644 --- a/src/Qwiq.Core.Rest/WorkItemStoreConfiguration.cs +++ b/src/Qwiq.Core.Rest/WorkItemStoreConfiguration.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Linq; namespace Microsoft.Qwiq.Client.Rest { @@ -17,7 +18,13 @@ public sealed class WorkItemStoreConfiguration : Qwiq.WorkItemStoreConfiguration CoreFieldRefNames.WorkItemType }; - private IEnumerable _defaultFields; + private static readonly string[] MandatoryFields = + { + CoreFieldRefNames.TeamProject, + CoreFieldRefNames.WorkItemType, + }; + + private HashSet _defaultFields; private WorkItemExpand _workItemExpand; @@ -28,9 +35,10 @@ internal WorkItemStoreConfiguration() } /// - /// Gets or sets a value indicating which fields to load when querying for a work item. Has no effect if is set to or . Defaults to , - /// , , , , + /// Gets or sets a value indicating which fields to load when querying for a work item. Has no effect if is set to + /// or . Defaults to , , + /// , , , , + /// , . /// public sealed override IEnumerable DefaultFields { @@ -45,7 +53,16 @@ public sealed override IEnumerable DefaultFields $"The {nameof(DefaultFields)} parameter can not be used with the {nameof(WorkItemExpand)} parameter. Setting {nameof(WorkItemExpand)} to {WorkItemExpand.None}."); _workItemExpand = WorkItemExpand.None; } - _defaultFields = value; + if (value == null || !value.Any()) _defaultFields.Clear(); + else + { + var fs = new HashSet(value, Comparer.OrdinalIgnoreCase); + foreach (var f in MandatoryFields) + { + fs.Add(f); + } + _defaultFields = fs; + } } } @@ -57,6 +74,9 @@ public sealed override IEnumerable DefaultFields /// /// Gets or sets a value indicating which sub-elements of to expand. Default value is . /// + /// + /// When value is set to , the value of is set to null. + /// public override WorkItemExpand WorkItemExpand { get => _workItemExpand; @@ -71,7 +91,7 @@ public override WorkItemExpand WorkItemExpand } else { - _defaultFields = DefaultFieldValue; + _defaultFields = new HashSet(DefaultFieldValue, Comparer.OrdinalIgnoreCase); } } } diff --git a/src/Qwiq.Core.Rest/WorkItemStoreFactory.cs b/src/Qwiq.Core.Rest/WorkItemStoreFactory.cs index 3231e621..3632f4bf 100644 --- a/src/Qwiq.Core.Rest/WorkItemStoreFactory.cs +++ b/src/Qwiq.Core.Rest/WorkItemStoreFactory.cs @@ -13,12 +13,6 @@ private WorkItemStoreFactory() { } - [Obsolete("This method is deprecated and will be removed in a future release. See property Default instead.", false)] - public static IWorkItemStoreFactory GetInstance() - { - return Default; - } - public override IWorkItemStore Create(AuthenticationOptions options) { if (options == null) throw new ArgumentNullException(nameof(options)); diff --git a/src/Qwiq.Core.Rest/packages.config b/src/Qwiq.Core.Rest/packages.config index 6a83c44a..7625c22d 100644 --- a/src/Qwiq.Core.Rest/packages.config +++ b/src/Qwiq.Core.Rest/packages.config @@ -2,7 +2,7 @@ - + diff --git a/src/Qwiq.Core.Soap/LevelOrderEnumerator.cs b/src/Qwiq.Core.Soap/LevelOrderEnumerator.cs new file mode 100644 index 00000000..38456366 --- /dev/null +++ b/src/Qwiq.Core.Soap/LevelOrderEnumerator.cs @@ -0,0 +1,83 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.TeamFoundation.WorkItemTracking.Client; + +namespace Microsoft.Qwiq.Client.Soap +{ + internal class LevelOrderEnumerator : IEnumerator + { + private readonly Queue _queue; + private int _currentGenerationCount; + private int _nextGenerationCount; + private readonly Node _root; + private readonly int? _maxDepth; + private int _currentDepth; + + public LevelOrderEnumerator(Node root, int? maxDepth = null) + { + _root = root; + _maxDepth = maxDepth; + _currentDepth = 0; + _queue = new Queue(); + _currentGenerationCount = 1; + _nextGenerationCount = 0; + Current = null; + } + + public void Dispose() + { + } + + public bool MoveNext() + { + if (Current == null) + { + Current = _root; + ProcessCurrent(); + + return true; + } + + if (_queue.Count == 0) + { + return false; + } + + if (_currentGenerationCount == 0) + { + _currentDepth++; + _currentGenerationCount = _nextGenerationCount; + _nextGenerationCount = 0; + } + + Current = _queue.Dequeue(); + ProcessCurrent(); + + return true; + } + + private void ProcessCurrent() + { + _currentGenerationCount--; + if (_currentDepth >= _maxDepth) return; + + Debug.Assert(Current != null, nameof(Current) + " != null"); + + foreach (Node child in Current.ChildNodes) + { + _nextGenerationCount++; + _queue.Enqueue(child); + } + } + + public void Reset() + { + Current = null; + } + + public Node Current { get; private set; } + + object IEnumerator.Current => Current; + } +} \ No newline at end of file diff --git a/src/Qwiq.Core.Soap/Node.cs b/src/Qwiq.Core.Soap/Node.cs deleted file mode 100644 index 4245c5b7..00000000 --- a/src/Qwiq.Core.Soap/Node.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Linq; - -using Tfs = Microsoft.TeamFoundation.WorkItemTracking.Client; - -namespace Microsoft.Qwiq.Client.Soap -{ - internal class Node : Qwiq.Node - { - internal Node(Tfs.Node node) - : base( - node.Id, - node.IsAreaNode, - node.IsIterationNode, - node.Name, - node.Uri, - () => node.ParentNode != null ? new Node(node.ParentNode) : null, - n => node.ChildNodes.Cast().Select(item => new Node(item)).ToList()) - { - if (node == null) throw new ArgumentNullException(nameof(node)); - - Path = node.Path; - } - - public override string Path { get; } - } -} \ No newline at end of file diff --git a/src/Qwiq.Core.Soap/Project.cs b/src/Qwiq.Core.Soap/Project.cs index 95262494..7cd16081 100644 --- a/src/Qwiq.Core.Soap/Project.cs +++ b/src/Qwiq.Core.Soap/Project.cs @@ -1,8 +1,4 @@ using System; -using System.Linq; - -using Microsoft.Qwiq.Exceptions; - using Tfs = Microsoft.TeamFoundation.WorkItemTracking.Client; namespace Microsoft.Qwiq.Client.Soap @@ -15,14 +11,9 @@ internal Project(Tfs.Project project) project.Name, project.Uri, new Lazy(() => new WorkItemTypeCollection(project.WorkItemTypes)), - new Lazy( - () => new NodeCollection( - project.AreaRootNodes.Cast() - .Select(item => ExceptionHandlingDynamicProxyFactory.Create(new Node(item))))), - new Lazy( - () => new NodeCollection( - project.IterationRootNodes.Cast() - .Select(item => ExceptionHandlingDynamicProxyFactory.Create(new Node(item)))))) + new Lazy>(()=> WorkItemClassificationNodeCollectionBuilder.Build(project.AreaRootNodes)), + new Lazy>(() => WorkItemClassificationNodeCollectionBuilder.Build(project.IterationRootNodes)) + ) { Id = project.Id; } diff --git a/src/Qwiq.Core.Soap/Qwiq.Client.Soap.csproj b/src/Qwiq.Core.Soap/Qwiq.Client.Soap.csproj index 5395aca5..1a64498a 100644 --- a/src/Qwiq.Core.Soap/Qwiq.Client.Soap.csproj +++ b/src/Qwiq.Core.Soap/Qwiq.Client.Soap.csproj @@ -1,6 +1,6 @@  - + @@ -186,12 +186,12 @@ + - @@ -208,6 +208,7 @@ + @@ -235,9 +236,9 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + diff --git a/src/Qwiq.Core.Soap/TeamFoundationIdentity.cs b/src/Qwiq.Core.Soap/TeamFoundationIdentity.cs index aeef7c4a..628058d4 100644 --- a/src/Qwiq.Core.Soap/TeamFoundationIdentity.cs +++ b/src/Qwiq.Core.Soap/TeamFoundationIdentity.cs @@ -14,48 +14,31 @@ internal class TeamFoundationIdentity : Qwiq.TeamFoundationIdentity private readonly Tfs.TeamFoundationIdentity _identity; - private readonly Lazy> _memberOf; - private readonly Lazy> _members; internal TeamFoundationIdentity(Tfs.TeamFoundationIdentity identity) - : base(identity.IsActive, identity.TeamFoundationId, identity.UniqueUserId) + : base( + identity.IsActive, + identity.TeamFoundationId, + identity.UniqueUserId, + identity.MemberOf.Select(i => new IdentityDescriptor(i)).ToArray(), + identity.Members.Select(i => new IdentityDescriptor(i)).ToArray()) { _identity = identity; _descriptor = new Lazy( () => ExceptionHandlingDynamicProxyFactory.Create( new IdentityDescriptor(_identity.Descriptor))); - - _memberOf = new Lazy>( - () => _identity.MemberOf.Select( - item => ExceptionHandlingDynamicProxyFactory.Create( - new IdentityDescriptor(item)))); - - _members = new Lazy>( - () => _identity.Members.Select( - item => ExceptionHandlingDynamicProxyFactory.Create( - new IdentityDescriptor(item)))); } public override IIdentityDescriptor Descriptor => _descriptor.Value; public override string DisplayName => _identity.DisplayName; - public override bool IsActive => _identity.IsActive; - public override bool IsContainer => _identity.IsContainer; - public override IEnumerable MemberOf => _memberOf.Value; - - public override IEnumerable Members => _members.Value; - - public override Guid TeamFoundationId => _identity.TeamFoundationId; - public override string UniqueName => _identity.UniqueName; - public override int UniqueUserId => _identity.UniqueUserId; - public override string GetAttribute(string name, string defaultValue) { return _identity.GetAttribute(name, defaultValue); diff --git a/src/Qwiq.Core.Soap/WorkItem.cs b/src/Qwiq.Core.Soap/WorkItem.cs index a6077830..f892b191 100644 --- a/src/Qwiq.Core.Soap/WorkItem.cs +++ b/src/Qwiq.Core.Soap/WorkItem.cs @@ -125,12 +125,6 @@ public override string IterationPath set => _item.IterationPath = value; } - public override string Keywords - { - get => (string)_item[WorkItemFields.Keywords]; - set => _item[WorkItemFields.Keywords] = value; - } - /// /// Gets the collection of the links in this work item. /// @@ -190,11 +184,6 @@ public override string Title set => _item.Title = value; } - /// - /// Gets the uniform resource identifier (System.Uri) of this work item. - /// - public override Uri Uri => _item.Uri; - public override string Url { get; } public override void ApplyRules(bool doNotUpdateChangedBy = false) @@ -224,13 +213,6 @@ public override IHyperlink CreateHyperlink(string location) return ExceptionHandlingDynamicProxyFactory.Create(new Hyperlink(new Tfs.Hyperlink(location))); } - public override IRelatedLink CreateRelatedLink(IWorkItemLinkTypeEnd linkTypeEnd, IWorkItem relatedWorkItem) - { - var rawLinkTypeEnd = LinkTypeEndMapper.Map(_item.Store, linkTypeEnd); - return ExceptionHandlingDynamicProxyFactory.Create( - new RelatedLink(new Tfs.RelatedLink(rawLinkTypeEnd, relatedWorkItem.Id))); - } - /// /// Validates the fields of this work item. /// diff --git a/src/Qwiq.Core.Soap/WorkItemClassificationNodeCollectionBuilder.cs b/src/Qwiq.Core.Soap/WorkItemClassificationNodeCollectionBuilder.cs new file mode 100644 index 00000000..31b01899 --- /dev/null +++ b/src/Qwiq.Core.Soap/WorkItemClassificationNodeCollectionBuilder.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Diagnostics; +using JetBrains.Annotations; +using Microsoft.TeamFoundation.WorkItemTracking.Client; + +namespace Microsoft.Qwiq.Client.Soap +{ + internal static class WorkItemClassificationNodeCollectionBuilder + { + public static IWorkItemClassificationNodeCollection Build(NodeCollection collection) + { + return new WorkItemClassificationNodeCollection(EnumerateNodeCollection(collection)); + } + + private static IEnumerable> EnumerateNodeCollection(NodeCollection collection) + { + foreach (Node n in collection) + { + var e = new LevelOrderEnumerator(n); + while (e.MoveNext()) + { + yield return BuildNode(e); + } + } + } + + private static IWorkItemClassificationNode BuildNode([NotNull] LevelOrderEnumerator e) + { + Debug.Assert(e.Current != null, "e.Current != null"); + + return new WorkItemClassificationNode( + e.Current.Id, + e.Current.IsAreaNode ? NodeType.Area : e.Current.IsIterationNode ? NodeType.Iteration : NodeType.None, + e.Current.Path, + e.Current.Uri + ); + } + } +} \ No newline at end of file diff --git a/src/Qwiq.Core.Soap/WorkItemLinkInfo.cs b/src/Qwiq.Core.Soap/WorkItemLinkInfo.cs index b0659bb4..7c4ef472 100644 --- a/src/Qwiq.Core.Soap/WorkItemLinkInfo.cs +++ b/src/Qwiq.Core.Soap/WorkItemLinkInfo.cs @@ -1,20 +1,16 @@ -using System; - -using JetBrains.Annotations; -#pragma warning disable 618 +using JetBrains.Annotations; namespace Microsoft.Qwiq.Client.Soap { - public class WorkItemLinkInfo : Qwiq.WorkItemLinkInfo + public class WorkItemLinkInfo : Qwiq.WorkItemLinkInfo, IIdentifiable { /// internal WorkItemLinkInfo(int sourceId, int targetId, int linkTypeId, [CanBeNull] IWorkItemLinkTypeEnd linkTypeEnd) : base(sourceId, targetId, linkTypeEnd) { - LinkTypeId = linkTypeId; + Id = linkTypeId; } - [Obsolete("This property is deprecated and will be removed in a future release.")] - public int LinkTypeId { get; } + public int Id { get; } } -} +} \ No newline at end of file diff --git a/src/Qwiq.Core.Soap/WorkItemStoreFactory.cs b/src/Qwiq.Core.Soap/WorkItemStoreFactory.cs index 670f2993..06495ae8 100644 --- a/src/Qwiq.Core.Soap/WorkItemStoreFactory.cs +++ b/src/Qwiq.Core.Soap/WorkItemStoreFactory.cs @@ -13,12 +13,6 @@ private WorkItemStoreFactory() { } - [Obsolete("This method is deprecated and will be removed in a future release. See property Default instead.", false)] - public static IWorkItemStoreFactory GetInstance() - { - return Default; - } - public override IWorkItemStore Create(AuthenticationOptions options) { if (options == null) throw new ArgumentNullException(nameof(options)); diff --git a/src/Qwiq.Core.Soap/WorkItemTypeCollection.cs b/src/Qwiq.Core.Soap/WorkItemTypeCollection.cs index be6d440a..78ae1bbb 100644 --- a/src/Qwiq.Core.Soap/WorkItemTypeCollection.cs +++ b/src/Qwiq.Core.Soap/WorkItemTypeCollection.cs @@ -19,8 +19,8 @@ internal WorkItemTypeCollection(TeamFoundation.WorkItemTracking.Client.WorkItemT public override int Count => _workItemTypeCollection.Count; - public override IWorkItemType this[string typeName] => ExceptionHandlingDynamicProxyFactory - .Create(new WorkItemType(_workItemTypeCollection[typeName])); + public override IWorkItemType this[string name] => ExceptionHandlingDynamicProxyFactory + .Create(new WorkItemType(_workItemTypeCollection[name])); public override IEnumerator GetEnumerator() { diff --git a/src/Qwiq.Core.Soap/packages.config b/src/Qwiq.Core.Soap/packages.config index c0ae2533..dcee6007 100644 --- a/src/Qwiq.Core.Soap/packages.config +++ b/src/Qwiq.Core.Soap/packages.config @@ -4,7 +4,7 @@ - + diff --git a/src/Qwiq.Core/Comparer.cs b/src/Qwiq.Core/Comparer.cs index e739aa05..a94c881b 100644 --- a/src/Qwiq.Core/Comparer.cs +++ b/src/Qwiq.Core/Comparer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; namespace Microsoft.Qwiq @@ -17,11 +18,12 @@ public static class Comparer public static IEqualityComparer IdentityDescriptor { get; } = IdentityDescriptorComparer.Default; - public static IEqualityComparer Node { get; } = NodeComparer.Default; - - public static IEqualityComparer> NodeCollection { get; } = - ReadOnlyCollectionWithIdComparer.Default; + public static IEqualityComparer> WorkItemClassificationNode { get; } = WorkItemClassificationNodeComparer.Default; + public static IEqualityComparer, int>> + WorkItemClassificationNodeCollection { get; } = + ReadOnlyCollectionWithIdComparer, int>.Default; + public static IEqualityComparer> NullableIdentity { get; } = NullableIdentifiableComparer.Default; public static IEqualityComparer OrdinalIgnoreCase { get; } = StringComparer.OrdinalIgnoreCase; diff --git a/src/Qwiq.Core/Credentials/CredentialsFactory.cs b/src/Qwiq.Core/Credentials/CredentialsFactory.cs index 8c951154..04b5536c 100644 --- a/src/Qwiq.Core/Credentials/CredentialsFactory.cs +++ b/src/Qwiq.Core/Credentials/CredentialsFactory.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Linq; using System.Net; using Microsoft.VisualStudio.Services.Client; @@ -20,67 +18,6 @@ namespace Microsoft.Qwiq.Credentials /// public static class CredentialsFactory { - [Obsolete( - "This method is deprecated and will be removed in a future release. See AuthenticationOptions instead.", - false)] - public static IEnumerable CreateCredentials( - string username = null, - string password = null, - string accessToken = null) - { - return CreateCredentials( - new Lazy(() => username), - new Lazy(() => password), - new Lazy(() => accessToken)); - } - - [Obsolete( - "This method is deprecated and will be removed in a future release. See AuthenticationOptions instead.", - false)] - public static IEnumerable CreateCredentials( - Lazy username = null, - Lazy password = null, - Lazy accessToken = null) - { - if (username == null) username = new Lazy(() => string.Empty); - if (password == null) password = new Lazy(() => string.Empty); - if (accessToken == null) accessToken = new Lazy(() => string.Empty); - - return CreateCredentialsImpl(username, password, accessToken).Select(c => new TfsCredentials(c)); - } - - private static IEnumerable CreateCredentialsImpl( - Lazy username, - Lazy password, - Lazy accessToken) - { - // First try OAuth, as this is our preferred method - foreach (var c in GetOAuthCredentials(accessToken.Value)) yield return c; - - // Next try Username/Password combinations - foreach (var c in GetServiceIdentityCredentials(username.Value, password.Value)) yield return c; - - // Next try PAT - foreach (var c in GetServiceIdentityPatCredentials(password.Value)) yield return c; - - // Next try basic credentials - foreach (var c in GetBasicCredentials(username.Value, password.Value)) yield return c; - - // User did not specify a username or a password, so use the process identity - yield return new VssClientCredentials(new WindowsCredential(false)) - { - Storage = new VssClientCredentialStorage(), - PromptType = CredentialPromptType.DoNotPrompt - }; - - // Use the Windows identity of the logged on user - yield return new VssClientCredentials(true) - { - Storage = new VssClientCredentialStorage(), - PromptType = CredentialPromptType.PromptIfNeeded - }; - } - internal static IEnumerable GetBasicCredentials(string username = null, string password = null) { if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) yield break; diff --git a/src/Qwiq.Core/Credentials/TfsCredentials.cs b/src/Qwiq.Core/Credentials/TfsCredentials.cs deleted file mode 100644 index 21a7d5d2..00000000 --- a/src/Qwiq.Core/Credentials/TfsCredentials.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Diagnostics; - -using Microsoft.VisualStudio.Services.Common; - -namespace Microsoft.Qwiq.Credentials -{ - [Obsolete("This type will be removed in a future release. Use VssCredentials instead.")] - [DebuggerStepThrough] - public sealed class TfsCredentials - { - public TfsCredentials(VssCredentials credentials) - { - Credentials = credentials ?? throw new ArgumentNullException(nameof(credentials)); - } - - internal VssCredentials Credentials { get; } - - public static implicit operator TfsCredentials(VssCredentials credentials) - { - return new TfsCredentials(credentials); - } - } -} \ No newline at end of file diff --git a/src/Qwiq.Core/Exceptions/ProxyBase.cs b/src/Qwiq.Core/Exceptions/ProxyBase.cs index 23b4f2b5..d639eb85 100644 --- a/src/Qwiq.Core/Exceptions/ProxyBase.cs +++ b/src/Qwiq.Core/Exceptions/ProxyBase.cs @@ -29,12 +29,16 @@ public override bool Equals(object obj) // ReSharper restore SuspiciousTypeConversion.Global if (proxy == null) { +#pragma warning disable RECS0149 // Finds potentially erroneous calls to Object.Equals return base.Equals(obj); +#pragma warning restore RECS0149 // Finds potentially erroneous calls to Object.Equals } var target = proxy.DynProxyGetTarget(); if (target == null) { +#pragma warning disable RECS0149 // Finds potentially erroneous calls to Object.Equals return base.Equals(obj); +#pragma warning restore RECS0149 // Finds potentially erroneous calls to Object.Equals } return target.Equals(obj); } diff --git a/src/Qwiq.Core/FieldDefinition.cs b/src/Qwiq.Core/FieldDefinition.cs index 5a6796d2..5043515d 100644 --- a/src/Qwiq.Core/FieldDefinition.cs +++ b/src/Qwiq.Core/FieldDefinition.cs @@ -1,8 +1,7 @@ -using System; +using JetBrains.Annotations; +using System; using System.Diagnostics.Contracts; -using JetBrains.Annotations; - namespace Microsoft.Qwiq { /// @@ -10,7 +9,6 @@ namespace Microsoft.Qwiq public class FieldDefinition : IFieldDefinition, IEquatable { internal FieldDefinition(int id, [NotNull] string referenceName, [NotNull] string name) - { Contract.Requires(!string.IsNullOrWhiteSpace(referenceName)); Contract.Requires(!string.IsNullOrWhiteSpace(name)); diff --git a/src/Qwiq.Core/GlobalSuppressions.cs b/src/Qwiq.Core/GlobalSuppressions.cs index 4edb1a4f..3c1a0c74 100644 Binary files a/src/Qwiq.Core/GlobalSuppressions.cs and b/src/Qwiq.Core/GlobalSuppressions.cs differ diff --git a/src/Qwiq.Core/IAttachment.cs b/src/Qwiq.Core/IAttachment.cs index 683048df..c4bd6798 100644 --- a/src/Qwiq.Core/IAttachment.cs +++ b/src/Qwiq.Core/IAttachment.cs @@ -2,7 +2,7 @@ namespace Microsoft.Qwiq { - public interface IAttachment + public interface IAttachment : IResourceReference { DateTime AttachedTime { get; } string Comment { get; set; } @@ -12,6 +12,5 @@ public interface IAttachment DateTime LastWriteTime { get; } long Length { get; } string Name { get; } - Uri Uri { get; } } } diff --git a/src/Qwiq.Core/IFieldDefinition.cs b/src/Qwiq.Core/IFieldDefinition.cs index e9bbc51d..3b2dfada 100644 --- a/src/Qwiq.Core/IFieldDefinition.cs +++ b/src/Qwiq.Core/IFieldDefinition.cs @@ -2,11 +2,8 @@ namespace Microsoft.Qwiq { - public interface IFieldDefinition : IIdentifiable + public interface IFieldDefinition : IIdentifiable, INamed { - [NotNull] - string Name { get; } - [NotNull] string ReferenceName { get; } } diff --git a/src/Qwiq.Core/INamed.cs b/src/Qwiq.Core/INamed.cs new file mode 100644 index 00000000..7587bf80 --- /dev/null +++ b/src/Qwiq.Core/INamed.cs @@ -0,0 +1,7 @@ +namespace Microsoft.Qwiq +{ + public interface INamed + { + string Name { get; } + } +} \ No newline at end of file diff --git a/src/Qwiq.Core/INode.cs b/src/Qwiq.Core/INode.cs deleted file mode 100644 index 1b468994..00000000 --- a/src/Qwiq.Core/INode.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace Microsoft.Qwiq -{ - public interface INode : IIdentifiable - { - INodeCollection ChildNodes { get; } - - bool HasChildNodes { get; } - - bool IsAreaNode { get; } - - bool IsIterationNode { get; } - - string Name { get; } - - INode ParentNode { get; } - - string Path { get; } - - Uri Uri { get; } - } -} \ No newline at end of file diff --git a/src/Qwiq.Core/INodeCollection.cs b/src/Qwiq.Core/INodeCollection.cs deleted file mode 100644 index 7c466e2b..00000000 --- a/src/Qwiq.Core/INodeCollection.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Microsoft.Qwiq -{ - public interface INodeCollection : IReadOnlyObjectWithIdCollection, IEquatable - { - } -} \ No newline at end of file diff --git a/src/Qwiq.Core/IProject.cs b/src/Qwiq.Core/IProject.cs index bf123230..411d1be3 100644 --- a/src/Qwiq.Core/IProject.cs +++ b/src/Qwiq.Core/IProject.cs @@ -2,17 +2,13 @@ namespace Microsoft.Qwiq { - public interface IProject : IIdentifiable + public interface IProject : IIdentifiable, IResourceReference, INamed { - INodeCollection AreaRootNodes { get; } + IWorkItemClassificationNodeCollection AreaRootNodes { get; } Guid Guid { get; } - INodeCollection IterationRootNodes { get; } - - string Name { get; } - - Uri Uri { get; } + IWorkItemClassificationNodeCollection IterationRootNodes { get; } IWorkItemTypeCollection WorkItemTypes { get; } } diff --git a/src/Qwiq.Core/IReadOnlyObjectCollection.cs b/src/Qwiq.Core/IReadOnlyObjectCollection.cs new file mode 100644 index 00000000..2477db7a --- /dev/null +++ b/src/Qwiq.Core/IReadOnlyObjectCollection.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace Microsoft.Qwiq +{ + public interface IReadOnlyObjectCollection : IReadOnlyList + { + /// + /// Determines whether the read-only collection contains a specific value. + /// + /// The object to locate in the read-only list. + /// true if the item is found; otherwise, false. + bool Contains(T value); + /// + /// Gets the element at the specified index in the read-only list. + /// + /// The zero-based index of the element to get. + /// The element at the specified index in the read-only list. + T GetItem(int index); + /// + /// Searches for the specified object and returns the index of its first occurrence in the read-only list. + /// + /// The object to locate in the read-only list. + /// A zero-based index of the first occurrence of in the read-only list; otherwise, -1. + int IndexOf(T value); + } +} \ No newline at end of file diff --git a/src/Qwiq.Core/IReadOnlyObjectList.cs b/src/Qwiq.Core/IReadOnlyObjectList.cs index 3e6bc1ae..65ce3784 100644 --- a/src/Qwiq.Core/IReadOnlyObjectList.cs +++ b/src/Qwiq.Core/IReadOnlyObjectList.cs @@ -1,12 +1,10 @@ -using System.Collections.Generic; - namespace Microsoft.Qwiq { /// /// Represents a read-only collection of elements that can be accessed by index or name. /// /// The type of elements in the read-only list. - public interface IReadOnlyObjectWithNameCollection : IReadOnlyList + public interface IReadOnlyObjectWithNameCollection : IReadOnlyObjectCollection { /// /// Gets the element of the specified name in the read-only list. @@ -15,13 +13,6 @@ public interface IReadOnlyObjectWithNameCollection : IReadOnlyList /// The element of the specified name in the read-only list. T this[string name] { get; } - /// - /// Determines whether the read-only collection contains a specific value. - /// - /// The object to locate in the read-only list. - /// true if the item is found; otherwise, false. - bool Contains(T value); - /// /// Determins whether the read-only collection contains an element with the specified name. /// @@ -29,20 +20,6 @@ public interface IReadOnlyObjectWithNameCollection : IReadOnlyList /// true if the item is found; otherwise, false. bool Contains(string name); - /// - /// Gets the element at the specified index in the read-only list. - /// - /// The zero-based index of the element to get. - /// The element at the specified index in the read-only list. - T GetItem(int index); - - /// - /// Searches for the specified object and returns the index of its first occurrence in the read-only list. - /// - /// The object to locate in the read-only list. - /// A zero-based index of the first occurrence of in the read-only list; otherwise, -1. - int IndexOf(T value); - /// /// Attempts to get the value associated with the specified name from the read-only list. /// diff --git a/src/Qwiq.Core/IRegisteredLinkType.cs b/src/Qwiq.Core/IRegisteredLinkType.cs index 362da333..b23d75eb 100644 --- a/src/Qwiq.Core/IRegisteredLinkType.cs +++ b/src/Qwiq.Core/IRegisteredLinkType.cs @@ -1,7 +1,6 @@ namespace Microsoft.Qwiq { - public interface IRegisteredLinkType + public interface IRegisteredLinkType : INamed { - string Name { get; } } } \ No newline at end of file diff --git a/src/Qwiq.Core/IRelatedLink.cs b/src/Qwiq.Core/IRelatedLink.cs index 7c2acf87..3592039c 100644 --- a/src/Qwiq.Core/IRelatedLink.cs +++ b/src/Qwiq.Core/IRelatedLink.cs @@ -4,9 +4,6 @@ namespace Microsoft.Qwiq { public interface IRelatedLink : ILink, IEquatable { - [Obsolete("This property is deprecated and will be removed in a future release. Use LinkTypeEnd.Name instead.")] - string LinkSubType { get; } - IWorkItemLinkTypeEnd LinkTypeEnd { get; } int RelatedWorkItemId { get; } diff --git a/src/Qwiq.Core/IResourceReference.cs b/src/Qwiq.Core/IResourceReference.cs new file mode 100644 index 00000000..0088814d --- /dev/null +++ b/src/Qwiq.Core/IResourceReference.cs @@ -0,0 +1,9 @@ +using System; + +namespace Microsoft.Qwiq +{ + public interface IResourceReference + { + Uri Uri { get; } + } +} \ No newline at end of file diff --git a/src/Qwiq.Core/IRevision.cs b/src/Qwiq.Core/IRevision.cs index 3e6882cd..cdfc44e5 100644 --- a/src/Qwiq.Core/IRevision.cs +++ b/src/Qwiq.Core/IRevision.cs @@ -1,6 +1,5 @@ -using System.Collections.Generic; - using JetBrains.Annotations; +using System.Collections.Generic; namespace Microsoft.Qwiq { @@ -46,8 +45,5 @@ public interface IRevision : IWorkItemCore /// /// Returns System.String. string GetTagLine(); - } -} - - +} \ No newline at end of file diff --git a/src/Qwiq.Core/ITfsTeamProjectCollection.cs b/src/Qwiq.Core/ITfsTeamProjectCollection.cs index 7a941609..15ae4edb 100644 --- a/src/Qwiq.Core/ITfsTeamProjectCollection.cs +++ b/src/Qwiq.Core/ITfsTeamProjectCollection.cs @@ -4,12 +4,7 @@ namespace Microsoft.Qwiq { - [Obsolete("This interface is deprecated and will be removed in a future version. Use ITeamProjectCollection instead.")] - public interface ITfsTeamProjectCollection : ITeamProjectCollection - { - } - - public interface ITeamProjectCollection + public interface ITeamProjectCollection : IResourceReference { /// Gets the credentials for this project collection. VssCredentials AuthorizedCredentials { get; } @@ -26,8 +21,5 @@ public interface ITeamProjectCollection /// This is used to convert dates and times to UTC. TimeZone TimeZone { get; } - - /// The base url for this connection - Uri Uri { get; } } } \ No newline at end of file diff --git a/src/Qwiq.Core/IWorkItem.cs b/src/Qwiq.Core/IWorkItem.cs index fc4676c6..92d4c228 100644 --- a/src/Qwiq.Core/IWorkItem.cs +++ b/src/Qwiq.Core/IWorkItem.cs @@ -32,8 +32,6 @@ public interface IWorkItem : IWorkItemCommon, IIdentifiable bool IsDirty { get; } - string Keywords { get; set; } - /// /// Gets the links of the work item in this revision. /// @@ -57,17 +55,8 @@ public interface IWorkItem : IWorkItemCommon, IIdentifiable /// /// Gets a Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemType object that represents the type of this work item. /// - /// - /// The Type property is null. - /// IWorkItemType Type { get; } - /// - /// Gets the uniform resource identifier (System.Uri) of this work item. - /// - [Obsolete("This property is deprecated and will be removed in a future release. See IWorkItemReference.Url instead.")] - Uri Uri { get; } - /// /// Applies the server rules for validation and fix up to the work item. /// @@ -94,9 +83,6 @@ public interface IWorkItem : IWorkItemCommon, IIdentifiable IHyperlink CreateHyperlink(string location); - [Obsolete("This method is deprecated and will be removed in a future release.")] - IRelatedLink CreateRelatedLink(IWorkItemLinkTypeEnd linkTypeEnd, IWorkItem relatedWorkItem); - IRelatedLink CreateRelatedLink(int relatedWorkItemId, IWorkItemLinkTypeEnd linkTypeEnd = null); /// diff --git a/src/Qwiq.Core/IWorkItemClassificationNode.cs b/src/Qwiq.Core/IWorkItemClassificationNode.cs new file mode 100644 index 00000000..ce404f7e --- /dev/null +++ b/src/Qwiq.Core/IWorkItemClassificationNode.cs @@ -0,0 +1,7 @@ +namespace Microsoft.Qwiq +{ + public interface IWorkItemClassificationNode : IIdentifiable, IResourceReference, INamed + { + NodeType Type { get; } + } +} diff --git a/src/Qwiq.Core/IWorkItemClassificationNodeCollection.cs b/src/Qwiq.Core/IWorkItemClassificationNodeCollection.cs new file mode 100644 index 00000000..499bffe7 --- /dev/null +++ b/src/Qwiq.Core/IWorkItemClassificationNodeCollection.cs @@ -0,0 +1,7 @@ +namespace Microsoft.Qwiq +{ + public interface IWorkItemClassificationNodeCollection : IReadOnlyObjectWithIdCollection, TId> + { + + } +} \ No newline at end of file diff --git a/src/Qwiq.Core/IWorkItemLinkInfo.Extensions.cs b/src/Qwiq.Core/IWorkItemLinkInfo.Extensions.cs new file mode 100644 index 00000000..5b85c3cd --- /dev/null +++ b/src/Qwiq.Core/IWorkItemLinkInfo.Extensions.cs @@ -0,0 +1,29 @@ +namespace Microsoft.Qwiq +{ + // ReSharper disable InconsistentNaming + public static class IWorkItemLinkInfoExtensions + // ReSharper restore InconsistentNaming + { + /// + /// Gets an Id for a . + /// + /// A with a + /// 0 if no link or link type; otherwise, the link type id. + /// + /// A true Id is only returned for SOAP instances of . + /// + /// + public static int LinkTypeId(this IWorkItemLinkInfo item) + { + if (item == null) return 0; + + // In SOAP, WorkItemLinkInfo is IIdentifiable, where the Id is the LinkTypeId + if (item is IIdentifiable i) + { + return i.Id; + } + + return item.LinkType.LinkTypeId(); + } + } +} diff --git a/src/Qwiq.Core/IWorkItemLinkType.Extensions.cs b/src/Qwiq.Core/IWorkItemLinkType.Extensions.cs new file mode 100644 index 00000000..86b7cfb0 --- /dev/null +++ b/src/Qwiq.Core/IWorkItemLinkType.Extensions.cs @@ -0,0 +1,29 @@ +namespace Microsoft.Qwiq +{ + // ReSharper disable InconsistentNaming + public static class IWorkItemLinkTypeExtensions + // ReSharper restore InconsistentNaming + { + /// + /// Gets the Id for the link type's forward end. + /// + /// An instance of . + /// 0 if no link or link type; otherwise, the link type id. + /// + public static int ForwardEndLinkTypeId(this IWorkItemLinkType item) + { + return item?.ForwardEnd.LinkTypeId() ?? 0; + } + + /// + /// Gets the Id for the link type's reverse end. + /// + /// An instance of . + /// 0 if no link or link type; otherwise, the link type id. + /// + public static int ReverseEndLinkTypeId(this IWorkItemLinkType item) + { + return item?.ReverseEnd.LinkTypeId() ?? 0; + } + } +} diff --git a/src/Qwiq.Core/IWorkItemLinkType.cs b/src/Qwiq.Core/IWorkItemLinkType.cs index 2327c32d..0d3c7f95 100644 --- a/src/Qwiq.Core/IWorkItemLinkType.cs +++ b/src/Qwiq.Core/IWorkItemLinkType.cs @@ -1,6 +1,6 @@ namespace Microsoft.Qwiq { - public interface IWorkItemLinkType + public interface IWorkItemLinkType : INamed { /// /// Link type of the link at the source work item. The appears when you add diff --git a/src/Qwiq.Core/IWorkItemLinkTypeEnd.Extensions.cs b/src/Qwiq.Core/IWorkItemLinkTypeEnd.Extensions.cs new file mode 100644 index 00000000..541346ff --- /dev/null +++ b/src/Qwiq.Core/IWorkItemLinkTypeEnd.Extensions.cs @@ -0,0 +1,54 @@ +using System; + +namespace Microsoft.Qwiq +{ + public static class IWorkItemLinkTypeEndExtensions + { + /// + /// Gets the Id from the specified . + /// + /// An instance of . + /// + /// 0 if is null or the is null; otherwise the link type id. + /// + /// + /// A true Id is only returned for SOAP instances of + /// + public static int LinkTypeId(this IWorkItemLinkTypeEnd item) + { + // No link type. In SOAP this is equivilent to SELF and has a constant id of 0 + if (item == null) + { + return 0; + } + + // In SOAP, the IWorkItemLinkTypeEnd is IIdentifiable. Try to cast and return the Id + if (item is IIdentifiable i) + { + return i.Id; + } + + // Same as initial case--no link type. + if (item.LinkType == null) + { + return 0; + } + + // Hack for REST: If there is an immutable name, get a case-insensitive hash + if (!string.IsNullOrEmpty(item.ImmutableName)) + { + var hash = Math.Abs(StringComparer.OrdinalIgnoreCase.GetHashCode(item.ImmutableName)); + // Forward links are ALWAYS a positive value + if (item.IsForwardLink) + { + return hash; + } + + // Reverse links are ALWAYS a negative value + return hash * -1; + } + + return 0; + } + } +} diff --git a/src/Qwiq.Core/IWorkItemLinkTypeEnd.cs b/src/Qwiq.Core/IWorkItemLinkTypeEnd.cs index f39054aa..4c073fc6 100644 --- a/src/Qwiq.Core/IWorkItemLinkTypeEnd.cs +++ b/src/Qwiq.Core/IWorkItemLinkTypeEnd.cs @@ -1,11 +1,10 @@ namespace Microsoft.Qwiq { - public interface IWorkItemLinkTypeEnd + public interface IWorkItemLinkTypeEnd : INamed { string ImmutableName { get; } bool IsForwardLink { get; } IWorkItemLinkType LinkType { get; } - string Name { get; } IWorkItemLinkTypeEnd OppositeEnd { get; } } } diff --git a/src/Qwiq.Core/IWorkItemStoreFactory.cs b/src/Qwiq.Core/IWorkItemStoreFactory.cs index cfb1e686..c09c9b0f 100644 --- a/src/Qwiq.Core/IWorkItemStoreFactory.cs +++ b/src/Qwiq.Core/IWorkItemStoreFactory.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; - using Microsoft.Qwiq.Credentials; namespace Microsoft.Qwiq @@ -8,17 +5,5 @@ namespace Microsoft.Qwiq public interface IWorkItemStoreFactory { IWorkItemStore Create(AuthenticationOptions options); - - [Obsolete( - "This method is deprecated and will be removed in a future release. See Create(AuthenticationOptions) instead.", - false)] - IWorkItemStore Create(Uri endpoint, TfsCredentials credentials); - - [Obsolete( - "This method is deprecated and will be removed in a future release. See Create(AuthenticationOptions) instead.", - false)] - IWorkItemStore Create( - Uri endpoint, - IEnumerable credentials); } } \ No newline at end of file diff --git a/src/Qwiq.Core/IWorkItemType.cs b/src/Qwiq.Core/IWorkItemType.cs index 715c7244..31ba372e 100644 --- a/src/Qwiq.Core/IWorkItemType.cs +++ b/src/Qwiq.Core/IWorkItemType.cs @@ -1,10 +1,8 @@ namespace Microsoft.Qwiq { - public interface IWorkItemType + public interface IWorkItemType : INamed { string Description { get; } - string Name { get; } - IFieldDefinitionCollection FieldDefinitions { get; } IWorkItem NewWorkItem(); } diff --git a/src/Qwiq.Core/IdentityFieldValue.cs b/src/Qwiq.Core/IdentityFieldValue.cs index e8eff624..42c1b263 100644 --- a/src/Qwiq.Core/IdentityFieldValue.cs +++ b/src/Qwiq.Core/IdentityFieldValue.cs @@ -1,11 +1,10 @@ -using Microsoft.VisualStudio.Services.Common; +using JetBrains.Annotations; +using Microsoft.VisualStudio.Services.Common; using System; using System.Diagnostics.Contracts; using System.Globalization; using System.Text.RegularExpressions; -using JetBrains.Annotations; - namespace Microsoft.Qwiq { /// @@ -91,7 +90,6 @@ public IdentityFieldValue(string displayName) if (string.IsNullOrEmpty(displayName)) return; - if (TryGetVsid(displayName, out Guid guid2, out string str)) { DisplayPart = str; diff --git a/src/Qwiq.Core/Link.cs b/src/Qwiq.Core/Link.cs index 9a8afe82..79a34618 100644 --- a/src/Qwiq.Core/Link.cs +++ b/src/Qwiq.Core/Link.cs @@ -17,9 +17,9 @@ protected internal Link([CanBeNull] string comment, BaseLinkType baseType) } /// - public virtual BaseLinkType BaseType { get; } + public BaseLinkType BaseType { get; } /// - public virtual string Comment { get; } + public string Comment { get; } } } \ No newline at end of file diff --git a/src/Qwiq.Core/Node.cs b/src/Qwiq.Core/Node.cs deleted file mode 100644 index 9cb9d058..00000000 --- a/src/Qwiq.Core/Node.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; - -namespace Microsoft.Qwiq -{ - public class Node : INode, IEquatable - { - private readonly Lazy _children; - - private readonly Lazy _parent; - - private readonly Lazy _path; - - internal Node( - int id, - bool isAreaNode, - bool isIterationNode, - string name, - Uri uri, - Func parentFactory, - Func> childrenFactory) - { - if (uri == null) throw new ArgumentNullException(nameof(uri)); - if (parentFactory == null) throw new ArgumentNullException(nameof(parentFactory)); - if (childrenFactory == null) throw new ArgumentNullException(nameof(childrenFactory)); - - Id = id; - IsAreaNode = isAreaNode; - IsIterationNode = isIterationNode; - Name = name ?? throw new ArgumentNullException(nameof(name)); - Uri = uri; - - _parent = new Lazy(parentFactory); - _children = new Lazy(() => new NodeCollection(childrenFactory(this))); - _path = new Lazy(() => ((ParentNode?.Path ?? string.Empty) + "\\" + Name).Trim('\\')); - } - - internal Node(int id, bool isAreaNode, bool isIterationNode, string name, Uri uri) - : this(id, isAreaNode, isIterationNode, name, uri, () => null, n => Enumerable.Empty()) - { - } - - public virtual INodeCollection ChildNodes => _children.Value; - - public virtual bool HasChildNodes => ChildNodes.Any(); - - public int Id { get; } - - public bool IsAreaNode { get; } - - public bool IsIterationNode { get; } - - public string Name { get; } - - public virtual INode ParentNode => _parent.Value; - - public virtual string Path => _path.Value; - - public virtual Uri Uri { get; } - - [DebuggerStepThrough] - public bool Equals(INode other) - { - return NodeComparer.Default.Equals(this, other); - } - - [DebuggerStepThrough] - public override bool Equals(object obj) - { - return NodeComparer.Default.Equals(this, obj as INode); - } - - [DebuggerStepThrough] - public override int GetHashCode() - { - return NodeComparer.Default.GetHashCode(this); - } - - [DebuggerStepThrough] - public override string ToString() - { - return Path; - } - } -} \ No newline at end of file diff --git a/src/Qwiq.Core/NodeCollection.cs b/src/Qwiq.Core/NodeCollection.cs deleted file mode 100644 index 30f04303..00000000 --- a/src/Qwiq.Core/NodeCollection.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Linq; - -using JetBrains.Annotations; - -namespace Microsoft.Qwiq -{ - public class NodeCollection : ReadOnlyObjectWithIdCollection, INodeCollection - { - internal NodeCollection([InstantHandle] [NotNull] IEnumerable nodes) - :this(nodes.ToList()) - { - Contract.Requires(nodes != null); - } - - internal NodeCollection(List nodes) - : base(nodes, node => node.Name) - { - } - - public bool Equals(INodeCollection other) - { - return Comparer.NodeCollection.Equals(this, other); - } - - public override bool Equals(object obj) - { - return Comparer.NodeCollection.Equals(this, obj as INodeCollection); - } - - public override int GetHashCode() - { - return Comparer.NodeCollection.GetHashCode(this); - } - } -} \ No newline at end of file diff --git a/src/Qwiq.Core/NodeType.cs b/src/Qwiq.Core/NodeType.cs new file mode 100644 index 00000000..e26d6369 --- /dev/null +++ b/src/Qwiq.Core/NodeType.cs @@ -0,0 +1,9 @@ +namespace Microsoft.Qwiq +{ + public enum NodeType : short + { + None, + Area, + Iteration + } +} \ No newline at end of file diff --git a/src/Qwiq.Core/NullableIdentifiableComparer.cs b/src/Qwiq.Core/NullableIdentifiableComparer.cs index 8b5e31cd..74070428 100644 --- a/src/Qwiq.Core/NullableIdentifiableComparer.cs +++ b/src/Qwiq.Core/NullableIdentifiableComparer.cs @@ -2,13 +2,11 @@ { internal class NullableIdentifiableComparer : GenericComparer> { - internal new static NullableIdentifiableComparer Default => Nested.Instance; - private NullableIdentifiableComparer() { - } + internal new static NullableIdentifiableComparer Default => Nested.Instance; public override bool Equals(IIdentifiable x, IIdentifiable y) { if (ReferenceEquals(x, y)) return true; diff --git a/src/Qwiq.Core/Project.cs b/src/Qwiq.Core/Project.cs index 8a6d30b7..b7e4f5d8 100644 --- a/src/Qwiq.Core/Project.cs +++ b/src/Qwiq.Core/Project.cs @@ -5,9 +5,9 @@ namespace Microsoft.Qwiq { public class Project : IProject, IEquatable { - private readonly Lazy _area; + private readonly Lazy> _area; - private readonly Lazy _iteration; + private readonly Lazy> _iteration; private readonly Lazy _wits; @@ -16,8 +16,8 @@ internal Project( string name, Uri uri, Lazy wits, - Lazy area, - Lazy iteration) + Lazy> area, + Lazy> iteration) { Guid = guid; Name = name != null ? string.Intern(name) : throw new ArgumentNullException(nameof(name)); @@ -36,11 +36,11 @@ public bool Equals(IProject other) return ProjectComparer.Default.Equals(this, other); } - public INodeCollection AreaRootNodes => _area.Value; + public IWorkItemClassificationNodeCollection AreaRootNodes => _area.Value; public Guid Guid { get; } - public INodeCollection IterationRootNodes => _iteration.Value; + public IWorkItemClassificationNodeCollection IterationRootNodes => _iteration.Value; public string Name { get; } diff --git a/src/Qwiq.Core/ProjectComparer.cs b/src/Qwiq.Core/ProjectComparer.cs index 817604e8..cd8daa18 100644 --- a/src/Qwiq.Core/ProjectComparer.cs +++ b/src/Qwiq.Core/ProjectComparer.cs @@ -28,9 +28,9 @@ public override int GetHashCode(IProject obj) var hash = 27; hash = (13 * hash) ^ obj.Guid.GetHashCode(); hash = (13 * hash) ^ (obj.Name != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Name) : 0); - hash = (13 * hash) ^ Comparer.NodeCollection.GetHashCode(obj.AreaRootNodes); - hash = (13 * hash) ^ Comparer.NodeCollection.GetHashCode(obj.IterationRootNodes); - hash = (13 * hash) ^ WorkItemTypeCollectionComparer.Default.GetHashCode(obj.WorkItemTypes); + hash = (13 * hash) ^ Comparer.WorkItemClassificationNodeCollection.GetHashCode(obj.AreaRootNodes); + hash = (13 * hash) ^ Comparer.WorkItemClassificationNodeCollection.GetHashCode(obj.IterationRootNodes); + hash = (13 * hash) ^ Comparer.WorkItemTypeCollection.GetHashCode(obj.WorkItemTypes); return hash; } diff --git a/src/Qwiq.Core/Qwiq.Core.csproj b/src/Qwiq.Core/Qwiq.Core.csproj index 64af66eb..ede24fb6 100644 --- a/src/Qwiq.Core/Qwiq.Core.csproj +++ b/src/Qwiq.Core/Qwiq.Core.csproj @@ -1,6 +1,6 @@  - + @@ -70,6 +70,7 @@ + @@ -83,7 +84,6 @@ - @@ -131,8 +131,7 @@ - - + @@ -141,11 +140,13 @@ + + @@ -155,13 +156,24 @@ + + + + IWorkItemLinkInfo.cs + + + IWorkItemLinkType.cs + + + IWorkItemLinkTypeEnd.cs + @@ -171,9 +183,7 @@ - - - + @@ -181,6 +191,7 @@ + @@ -197,6 +208,8 @@ + + @@ -234,8 +247,8 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + \ No newline at end of file diff --git a/src/Qwiq.Core/ReadOnlyObjectCollection.cs b/src/Qwiq.Core/ReadOnlyObjectCollection.cs new file mode 100644 index 00000000..685b1802 --- /dev/null +++ b/src/Qwiq.Core/ReadOnlyObjectCollection.cs @@ -0,0 +1,149 @@ +using JetBrains.Annotations; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Qwiq +{ + public abstract class ReadOnlyObjectCollection : IReadOnlyObjectCollection + { + private readonly object _lockObj = new object(); + private bool _alreadyInit; + private Func> _itemFactory; + private Lazy> _lazyItems; + + protected ReadOnlyObjectCollection([NotNull] Func> itemFactory) + { + ItemFactory = itemFactory ?? throw new ArgumentNullException(nameof(itemFactory)); + } + + protected ReadOnlyObjectCollection([CanBeNull] List items) + : this() + { + List = items ?? new List(0); + _alreadyInit = false; + } + + protected ReadOnlyObjectCollection([CanBeNull] IEnumerable items) + : this(() => items) + { + } + + protected ReadOnlyObjectCollection() + { + Initialize(); + } + + protected internal List List { get; set; } + + public virtual int Count + { + get + { + Ensure(); + return List.Count; + } + } + + protected Func> ItemFactory + { + get => _itemFactory; + set + { + _itemFactory = value; + lock (_lockObj) + { + _lazyItems = new Lazy>(_itemFactory); + Initialize(); + } + } + } + + public virtual T this[int index] + { + get + { + Ensure(); + if (index < 0 || index >= List.Count) throw new ArgumentOutOfRangeException(nameof(index)); + return List[index]; + } + } + + [DebuggerStepThrough] + public virtual bool Contains(T value) + { + return IndexOf(value) != -1; + } + + [DebuggerStepThrough] + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + [DebuggerStepThrough] + public virtual IEnumerator GetEnumerator() + { + Ensure(); + return List.GetEnumerator(); + } + + T IReadOnlyObjectCollection.GetItem(int index) + { + return GetItem(index); + } + + public virtual int IndexOf(T value) + { + Ensure(); + for (var i = 0; i < Count; i++) if (GenericComparer.Default.Equals(this[i], value)) return i; + + return -1; + } + + protected int Add(T value) + { + var index = List.Count; + + Add(value, index); + + List.Add(value); + return index; + } + + protected virtual void Add(T value, int index) + { + // Available for hooking Add operations + } + + protected void Ensure() + { + if (!_alreadyInit) + lock (_lockObj) + { + if (!_alreadyInit) + { + if (_lazyItems != null) foreach (var item in _lazyItems.Value) Add(item); + else for (var i = 0; i < List.Count; i++) Add(List[i], i); + + _alreadyInit = true; + _lazyItems = null; + } + } + } + protected virtual T GetItem(int index) + { + return this[index]; + } + + private void Initialize() + { + lock (_lockObj) + { + List = new List(); + _alreadyInit = false; + } + } + } +} \ No newline at end of file diff --git a/src/Qwiq.Core/ReadOnlyObjectWithNameCollection.cs b/src/Qwiq.Core/ReadOnlyObjectWithNameCollection.cs index 37864230..d22b1875 100644 --- a/src/Qwiq.Core/ReadOnlyObjectWithNameCollection.cs +++ b/src/Qwiq.Core/ReadOnlyObjectWithNameCollection.cs @@ -1,33 +1,28 @@ -using System; -using System.Collections; +using JetBrains.Annotations; +using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.Contracts; -using JetBrains.Annotations; - namespace Microsoft.Qwiq { /// /// Base class for common operations for Collections. /// /// - public abstract class ReadOnlyObjectWithNameCollection : IReadOnlyObjectWithNameCollection + public abstract class ReadOnlyObjectWithNameCollection : ReadOnlyObjectCollection, IReadOnlyObjectWithNameCollection + //TODO: Restrict T to INamed { + private readonly object _lockObj = new object(); [CanBeNull] private readonly Func _nameFunc; - - private bool _alreadyInit; - - private Func> _itemFactory; - - private Lazy> _lazyItems; - private IDictionary _mapByName; - protected ReadOnlyObjectWithNameCollection([NotNull] Func> itemFactory, [CanBeNull] Func nameFunc) + protected ReadOnlyObjectWithNameCollection( + [NotNull] Func> itemFactory, + [CanBeNull] Func nameFunc) + : this() { Contract.Requires(itemFactory != null); Contract.Requires(nameFunc != null); @@ -37,15 +32,14 @@ protected ReadOnlyObjectWithNameCollection([NotNull] Func> itemFa } protected ReadOnlyObjectWithNameCollection([CanBeNull] List items, [CanBeNull] Func nameFunc) + : base(items) { - List = items ?? new List(0); - _mapByName = new Dictionary(StringComparer.OrdinalIgnoreCase); _nameFunc = nameFunc; - _alreadyInit = false; + Initialize(); } protected ReadOnlyObjectWithNameCollection([CanBeNull] IEnumerable items) - : this(()=> items, null) + : this(() => items, null) { } @@ -55,42 +49,16 @@ protected ReadOnlyObjectWithNameCollection([CanBeNull] List items) } protected ReadOnlyObjectWithNameCollection() + : base() { Initialize(); } - public virtual int Count - { - get - { - Ensure(); - return List.Count; - } - } - - protected internal List List { get; private set; } - - protected Func> ItemFactory - { - get => _itemFactory; - set - { - _itemFactory = value; - lock (_lockObj) - { - _lazyItems = new Lazy>(_itemFactory); - Initialize(); - } - } - } - - public virtual T this[int index] + private void Initialize() { - get + lock (_lockObj) { - Ensure(); - if (index < 0 || index >= List.Count) throw new ArgumentOutOfRangeException(nameof(index)); - return List[index]; + _mapByName = new Dictionary(StringComparer.OrdinalIgnoreCase); } } @@ -106,12 +74,6 @@ public virtual T this[string name] } } - [DebuggerStepThrough] - public virtual bool Contains(T value) - { - return IndexOf(value) != -1; - } - public virtual bool Contains(string name) { if (name == null) throw new ArgumentNullException(nameof(name)); @@ -119,21 +81,6 @@ public virtual bool Contains(string name) return _mapByName.ContainsKey(name); } - [DebuggerStepThrough] - public virtual IEnumerator GetEnumerator() - { - Ensure(); - return List.GetEnumerator(); - } - - public virtual int IndexOf(T value) - { - Ensure(); - for (var i = 0; i < Count; i++) if (GenericComparer.Default.Equals(this[i], value)) return i; - - return -1; - } - public virtual bool TryGetByName(string name, out T value) { if (string.IsNullOrEmpty(name)) @@ -152,18 +99,7 @@ public virtual bool TryGetByName(string name, out T value) return false; } - [DebuggerStepThrough] - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - T IReadOnlyObjectWithNameCollection.GetItem(int index) - { - return GetItem(index); - } - - protected virtual void Add(T value, int index) + protected override void Add(T value, int index) { if (_nameFunc != null) { @@ -172,16 +108,6 @@ protected virtual void Add(T value, int index) } } - protected int Add(T value) - { - var index = List.Count; - - Add(value, index); - - List.Add(value); - return index; - } - protected void AddByName(string name, int index) { try @@ -193,36 +119,5 @@ protected void AddByName(string name, int index) throw new ArgumentException($"An item with the name {name} already exists.", e); } } - - protected void Ensure() - { - if (!_alreadyInit) - lock (_lockObj) - { - if (!_alreadyInit) - { - if (_lazyItems != null) foreach (var item in _lazyItems.Value) Add(item); - else for (var i = 0; i < List.Count; i++) Add(List[i], i); - - _alreadyInit = true; - _lazyItems = null; - } - } - } - - protected virtual T GetItem(int index) - { - return this[index]; - } - - private void Initialize() - { - lock (_lockObj) - { - List = new List(); - _mapByName = new Dictionary(StringComparer.OrdinalIgnoreCase); - _alreadyInit = false; - } - } } } \ No newline at end of file diff --git a/src/Qwiq.Core/RelatedLink.cs b/src/Qwiq.Core/RelatedLink.cs index fc88e2f1..a4644474 100644 --- a/src/Qwiq.Core/RelatedLink.cs +++ b/src/Qwiq.Core/RelatedLink.cs @@ -1,5 +1,4 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using JetBrains.Annotations; @@ -15,9 +14,6 @@ internal RelatedLink(int related, [CanBeNull] IWorkItemLinkTypeEnd linkTypeEnd = LinkTypeEnd = linkTypeEnd; } - [Obsolete("This property is deprecated and will be removed in a future release. Use LinkTypeEnd.Name instead.")] - public string LinkSubType => LinkTypeEnd?.Name; - public IWorkItemLinkTypeEnd LinkTypeEnd { get; } public int RelatedWorkItemId { get; } diff --git a/src/Qwiq.Core/TeamFoundationIdentity.cs b/src/Qwiq.Core/TeamFoundationIdentity.cs index 24b60708..7faef3ab 100644 --- a/src/Qwiq.Core/TeamFoundationIdentity.cs +++ b/src/Qwiq.Core/TeamFoundationIdentity.cs @@ -7,15 +7,9 @@ namespace Microsoft.Qwiq { public abstract class TeamFoundationIdentity : ITeamFoundationIdentity, IEquatable { - private string _uniqueName; protected internal static readonly IIdentityDescriptor[] ZeroLengthArrayOfIdentityDescriptor = new IIdentityDescriptor[0]; + private string _uniqueName; - private TeamFoundationIdentity() - { - MemberOf = ZeroLengthArrayOfIdentityDescriptor; - Members = ZeroLengthArrayOfIdentityDescriptor; - TeamFoundationId = Guid.Empty; - } protected internal TeamFoundationIdentity( bool isActive, Guid teamFoundationId, @@ -28,16 +22,30 @@ int uniqueUserId UniqueUserId = uniqueUserId; } - public bool Equals(ITeamFoundationIdentity other) + protected internal TeamFoundationIdentity( + bool isActive, + Guid teamFoundationId, + int uniqueUserId, + IEnumerable memberOf, + IEnumerable members) + : this(isActive, teamFoundationId, uniqueUserId) { - return TeamFoundationIdentityComparer.Default.Equals(this, other); + MemberOf = memberOf ?? ZeroLengthArrayOfIdentityDescriptor; + Members = members ?? ZeroLengthArrayOfIdentityDescriptor; + } + + private TeamFoundationIdentity() + { + MemberOf = ZeroLengthArrayOfIdentityDescriptor; + Members = ZeroLengthArrayOfIdentityDescriptor; + TeamFoundationId = Guid.Empty; } public abstract IIdentityDescriptor Descriptor { get; } public abstract string DisplayName { get; } - public virtual bool IsActive { get; } + public bool IsActive { get; } public virtual bool IsContainer { @@ -56,11 +64,11 @@ public virtual bool IsContainer } } - public virtual IEnumerable MemberOf { get; } + public IEnumerable MemberOf { get; } - public virtual IEnumerable Members { get; } + public IEnumerable Members { get; } - public virtual Guid TeamFoundationId { get; } + public Guid TeamFoundationId { get; } public virtual string UniqueName { @@ -91,24 +99,29 @@ public virtual string UniqueName } } - public virtual int UniqueUserId { get; } - - public abstract string GetAttribute(string name, string defaultValue); - - public abstract IEnumerable> GetProperties(); + public int UniqueUserId { get; } - public abstract object GetProperty(string name); + public bool Equals(ITeamFoundationIdentity other) + { + return Comparer.TeamFoundationIdentity.Equals(this, other); + } public override bool Equals(object obj) { return Equals(obj as ITeamFoundationIdentity); } + public abstract string GetAttribute(string name, string defaultValue); + public override int GetHashCode() { - return TeamFoundationIdentityComparer.Default.GetHashCode(this); + return Comparer.TeamFoundationIdentity.GetHashCode(this); } + public abstract IEnumerable> GetProperties(); + + public abstract object GetProperty(string name); + public override string ToString() { // Call of .ToString to avoid boxing Guid to Object diff --git a/src/Qwiq.Core/TypeParser.cs b/src/Qwiq.Core/TypeParser.cs index 83ae65ae..b82b1fdd 100644 --- a/src/Qwiq.Core/TypeParser.cs +++ b/src/Qwiq.Core/TypeParser.cs @@ -208,7 +208,9 @@ private static bool TryConvert(Type destinationType, object value, out object re return true; } // ReSharper disable CatchAllClause +#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body catch +#pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body // ReSharper restore CatchAllClause { } @@ -223,7 +225,9 @@ private static bool TryConvert(Type destinationType, object value, out object re return true; } // ReSharper disable CatchAllClause +#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body catch +#pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body // ReSharper restore CatchAllClause { } @@ -236,7 +240,9 @@ private static bool TryConvert(Type destinationType, object value, out object re return true; } // ReSharper disable CatchAllClause +#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body catch +#pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body // ReSharper restore CatchAllClause { } @@ -252,7 +258,9 @@ private static bool TryConvert(Type destinationType, object value, out object re return true; } // ReSharper disable EmptyGeneralCatchClause +#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body catch +#pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body // ReSharper restore EmptyGeneralCatchClause { } diff --git a/src/Qwiq.Core/ValidationState.cs b/src/Qwiq.Core/ValidationState.cs index b1cb0a94..2e2085ca 100644 --- a/src/Qwiq.Core/ValidationState.cs +++ b/src/Qwiq.Core/ValidationState.cs @@ -1,11 +1,9 @@ namespace Microsoft.Qwiq { /// - /// + /// A description of the current state of a field. /// - /// - /// See - /// + // See Microsoft.TeamFoundation.WorkItemTracking.Client.FieldStatus public enum ValidationState { Valid = 0, diff --git a/src/Qwiq.Core/WorkItem.cs b/src/Qwiq.Core/WorkItem.cs index f83f17ce..abe357c7 100644 --- a/src/Qwiq.Core/WorkItem.cs +++ b/src/Qwiq.Core/WorkItem.cs @@ -92,12 +92,6 @@ public virtual IFieldCollection Fields public virtual bool IsDirty => throw new NotSupportedException(); - public virtual string Keywords - { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); - } - public virtual ICollection Links => throw new NotSupportedException(); public new virtual int RelatedLinkCount => base.RelatedLinkCount.GetValueOrDefault(0); @@ -112,8 +106,6 @@ public virtual string Keywords public virtual IWorkItemType Type => _type ?? _lazyType?.Value ?? throw new InvalidOperationException($"No value specified for {nameof(Type)}."); - public abstract Uri Uri { get; } - public override object this[string name] { get @@ -171,11 +163,6 @@ public virtual IHyperlink CreateHyperlink(string location) throw new NotSupportedException(); } - public virtual IRelatedLink CreateRelatedLink(IWorkItemLinkTypeEnd linkTypeEnd, IWorkItem relatedWorkItem) - { - return CreateRelatedLink(relatedWorkItem.Id, linkTypeEnd); - } - public virtual IRelatedLink CreateRelatedLink(int relatedWorkItemId, IWorkItemLinkTypeEnd linkTypeEnd = null) { throw new NotSupportedException(); diff --git a/src/Qwiq.Core/WorkItemClassificationNode.cs b/src/Qwiq.Core/WorkItemClassificationNode.cs new file mode 100644 index 00000000..d29e3052 --- /dev/null +++ b/src/Qwiq.Core/WorkItemClassificationNode.cs @@ -0,0 +1,53 @@ +using System; +using System.Diagnostics; +using JetBrains.Annotations; + +namespace Microsoft.Qwiq +{ + + public class WorkItemClassificationNode : IWorkItemClassificationNode, IEquatable> + { + + + public WorkItemClassificationNode(TId id, NodeType nodeType, [NotNull] string name, Uri uri) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); + + + Name = name; + Uri = uri; + Id = id; + Type = nodeType; + } + + public TId Id { get; } + public Uri Uri { get; } + public NodeType Type { get; } + public string Name { get; } + + [DebuggerStepThrough] + public bool Equals(IWorkItemClassificationNode other) + { + return WorkItemClassificationNodeComparer.Default.Equals(this, other); + } + + [DebuggerStepThrough] + public override bool Equals(object obj) + { + return WorkItemClassificationNodeComparer.Default.Equals(this, obj as IWorkItemClassificationNode); + } + + [DebuggerStepThrough] + public override int GetHashCode() + { + return WorkItemClassificationNodeComparer.Default.GetHashCode(this); + } + + [DebuggerStepThrough] + public override string ToString() + { + return Name; + } + } +} diff --git a/src/Qwiq.Core/WorkItemClassificationNodeCollection.cs b/src/Qwiq.Core/WorkItemClassificationNodeCollection.cs new file mode 100644 index 00000000..86a424e9 --- /dev/null +++ b/src/Qwiq.Core/WorkItemClassificationNodeCollection.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Microsoft.Qwiq +{ + public class WorkItemClassificationNodeCollection : ReadOnlyObjectWithIdCollection, TId>, IEquatable>, IWorkItemClassificationNodeCollection + { + public WorkItemClassificationNodeCollection([CanBeNull] IEnumerable> items) : base(items) + { + } + + public bool Equals(IWorkItemClassificationNodeCollection other) + { + return ReadOnlyCollectionWithIdComparer, TId>.Default.Equals(this, other); + } + + public override bool Equals(object obj) + { + return ReadOnlyCollectionWithIdComparer, TId>.Default.Equals(this, obj as IWorkItemClassificationNodeCollection); + } + + public override int GetHashCode() + { + return ReadOnlyCollectionWithIdComparer, TId>.Default.GetHashCode(this); + } + } +} \ No newline at end of file diff --git a/src/Qwiq.Core/NodeComparer.cs b/src/Qwiq.Core/WorkItemClassificationNodeComparer.cs similarity index 52% rename from src/Qwiq.Core/NodeComparer.cs rename to src/Qwiq.Core/WorkItemClassificationNodeComparer.cs index e7e7488f..0a0bb219 100644 --- a/src/Qwiq.Core/NodeComparer.cs +++ b/src/Qwiq.Core/WorkItemClassificationNodeComparer.cs @@ -1,31 +1,28 @@ using System; - using JetBrains.Annotations; namespace Microsoft.Qwiq { - internal class NodeComparer : GenericComparer + internal class WorkItemClassificationNodeComparer : GenericComparer> { - internal new static readonly NodeComparer Default = Nested.Instance; + internal static new readonly WorkItemClassificationNodeComparer Default = Nested.Instance; - private NodeComparer() + private WorkItemClassificationNodeComparer() { } - public override bool Equals(INode x, INode y) + public override bool Equals(IWorkItemClassificationNode x, IWorkItemClassificationNode y) { if (ReferenceEquals(x, y)) return true; if (ReferenceEquals(x, null)) return false; if (ReferenceEquals(y, null)) return false; - return x.Id == y.Id - && x.IsAreaNode == y.IsAreaNode - && x.IsIterationNode == y.IsIterationNode - && string.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase) - && string.Equals(x.Path, y.Path, StringComparison.OrdinalIgnoreCase); + return GenericComparer.Default.Equals(x.Id, y.Id) + && x.Type == y.Type + && string.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase); } - public override int GetHashCode([CanBeNull] INode obj) + public override int GetHashCode([CanBeNull] IWorkItemClassificationNode obj) { if (ReferenceEquals(obj, null)) return 0; @@ -33,11 +30,9 @@ public override int GetHashCode([CanBeNull] INode obj) { var hash = 27; - hash = (hash * 13) ^ obj.Id; - hash = (hash * 13) ^ obj.IsAreaNode.GetHashCode(); - hash = (hash * 13) ^ obj.IsIterationNode.GetHashCode(); + hash = (hash * 13) ^ GenericComparer.Default.GetHashCode(obj.Id); hash = (hash * 13) ^ (obj.Name != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Name) : 0); - hash = (hash * 13) ^ (obj.Path != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Path) : 0); + hash = (hash * 13) ^ obj.Type.GetHashCode(); return hash; } @@ -46,7 +41,7 @@ public override int GetHashCode([CanBeNull] INode obj) private class Nested { // ReSharper disable MemberHidesStaticFromOuterClass - internal static readonly NodeComparer Instance = new NodeComparer(); + internal static readonly WorkItemClassificationNodeComparer Instance = new WorkItemClassificationNodeComparer(); // ReSharper restore MemberHidesStaticFromOuterClass diff --git a/src/Qwiq.Core/WorkItemCore.cs b/src/Qwiq.Core/WorkItemCore.cs index 226e809e..a624fc44 100644 --- a/src/Qwiq.Core/WorkItemCore.cs +++ b/src/Qwiq.Core/WorkItemCore.cs @@ -20,11 +20,9 @@ protected internal WorkItemCore([CanBeNull] Dictionary fields) _fields = fields ?? new Dictionary(StringComparer.OrdinalIgnoreCase); } - public abstract string Url { get; } public virtual int? Id => GetValue(CoreFieldRefNames.Id); - public virtual int? Rev => GetValue(CoreFieldRefNames.Rev); - + public abstract string Url { get; } /// /// Gets or sets the with the specified name. /// @@ -42,7 +40,6 @@ protected internal WorkItemCore([CanBeNull] Dictionary fields) public virtual object this[string name] { get - { if (name == null) throw new ArgumentNullException(nameof(name)); return GetValue(name); diff --git a/src/Qwiq.Core/WorkItemLinkType.cs b/src/Qwiq.Core/WorkItemLinkType.cs index 38377ec0..2ff45f5e 100644 --- a/src/Qwiq.Core/WorkItemLinkType.cs +++ b/src/Qwiq.Core/WorkItemLinkType.cs @@ -101,5 +101,7 @@ private IWorkItemLinkTypeEnd CoerceReverseValue() { return _reverse ?? (_reverse = _reverseFac.Value); } + + public string Name => ReferenceName; } } \ No newline at end of file diff --git a/src/Qwiq.Core/WorkItemStoreFactory.cs b/src/Qwiq.Core/WorkItemStoreFactory.cs index be5998b5..a724ff13 100644 --- a/src/Qwiq.Core/WorkItemStoreFactory.cs +++ b/src/Qwiq.Core/WorkItemStoreFactory.cs @@ -1,7 +1,4 @@ using Microsoft.Qwiq.Credentials; -using System; -using System.Collections.Generic; -using System.Linq; namespace Microsoft.Qwiq { @@ -9,18 +6,5 @@ public abstract class WorkItemStoreFactory : IWorkItemStoreFactory { /// public abstract IWorkItemStore Create(AuthenticationOptions options); - - [Obsolete("This method is deprecated and will be removed in a future release. See Create(AuthenticationOptions) instead.", false)] - public virtual IWorkItemStore Create(Uri endpoint, TfsCredentials credentials) - { - return Create(endpoint, new[] { credentials }); - } - - [Obsolete("This method is deprecated and will be removed in a future release. See Create(AuthenticationOptions) instead.", false)] - public virtual IWorkItemStore Create(Uri endpoint, IEnumerable credentials) - { - var options = new AuthenticationOptions(endpoint, AuthenticationTypes.Windows, types => credentials.Select(s => s.Credentials)); - return Create(options); - } } } \ No newline at end of file diff --git a/src/Qwiq.Core/packages.config b/src/Qwiq.Core/packages.config index eab3ad6c..b0d51296 100644 --- a/src/Qwiq.Core/packages.config +++ b/src/Qwiq.Core/packages.config @@ -4,7 +4,7 @@ - + diff --git a/src/Qwiq.Identity.Soap/IdentityManagementService.cs b/src/Qwiq.Identity.Soap/IdentityManagementService.cs index c847f8b8..099f43e5 100644 --- a/src/Qwiq.Identity.Soap/IdentityManagementService.cs +++ b/src/Qwiq.Identity.Soap/IdentityManagementService.cs @@ -24,20 +24,28 @@ public IIdentityDescriptor CreateIdentityDescriptor(string identityType, string public IEnumerable ReadIdentities(IEnumerable descriptors) { + return ReadIdentities(descriptors, MembershipQuery.None); + } + + public IEnumerable ReadIdentities( + IEnumerable descriptors, + MembershipQuery queryMembership) + { + if (descriptors == null) throw new ArgumentNullException(nameof(descriptors)); + var rawDescriptors = descriptors.Select( - descriptor => new TeamFoundation.Framework.Client.IdentityDescriptor( - descriptor - .IdentityType, - descriptor - .Identifier)) - .ToArray(); + descriptor => new TeamFoundation.Framework.Client.IdentityDescriptor( + descriptor.IdentityType, + descriptor.Identifier)) + .ToArray(); var identities = - _identityManagementService2.ReadIdentities( - rawDescriptors, - MembershipQuery.None, - ReadIdentityOptions.IncludeReadFromSource); + _identityManagementService2.ReadIdentities( + rawDescriptors, + (TeamFoundation.Framework.Common.MembershipQuery)queryMembership, + ReadIdentityOptions.IncludeReadFromSource); + // TODO: Use configuration options from IWorkItemStore to control proxy creation return identities.Select(identity => identity?.AsProxy()); } @@ -45,33 +53,57 @@ public IEnumerable>> R IdentitySearchFactor searchFactor, IEnumerable searchFactorValues) { + return ReadIdentities(searchFactor, searchFactorValues, MembershipQuery.None); + } + + public IEnumerable>> ReadIdentities( + IdentitySearchFactor searchFactor, + IEnumerable searchFactorValues, + MembershipQuery queryMembership) + { + if (searchFactorValues == null) throw new ArgumentNullException(nameof(searchFactorValues)); + var searchFactorArray = searchFactorValues.ToArray(); var factor = (TeamFoundation.Framework.Common.IdentitySearchFactor)searchFactor; var identities = _identityManagementService2.ReadIdentities( - factor, - searchFactorArray, - MembershipQuery.None, - ReadIdentityOptions.IncludeReadFromSource); + factor, + searchFactorArray, + (TeamFoundation.Framework.Common.MembershipQuery)queryMembership, + ReadIdentityOptions.IncludeReadFromSource); if (searchFactorArray.Length != identities.Length) throw new IndexOutOfRangeException( - "A call to IIdentityManagementService2.ReadIdentities resulted in a return set where there was not a one to one mapping between search terms and search results. This is unexpected behavior and execution cannot continue. Please check if the underlying service implementation has changed and update the consuming code as appropriate."); + "A call to IIdentityManagementService2.ReadIdentities resulted in a return set where there was not a one to one mapping between search terms and search results. This is unexpected behavior and execution cannot continue. Please check if the underlying service implementation has changed and update the consuming code as appropriate."); for (var i = 0; i < searchFactorArray.Length; i++) { - var proxiedIdentities = identities[i].Select(id=>id.AsProxy()); + // TODO: Use configuration options from IWorkItemStore to control proxy creation + var proxiedIdentities = identities[i].Select(id => id.AsProxy()); yield return new KeyValuePair>(searchFactorArray[i], proxiedIdentities); } } - public ITeamFoundationIdentity ReadIdentity(IdentitySearchFactor searchFactor, string searchFactorValue) + public ITeamFoundationIdentity ReadIdentity( + IdentitySearchFactor searchFactor, + string searchFactorValue) + { + return ReadIdentity(searchFactor, searchFactorValue, MembershipQuery.None); + } + + public ITeamFoundationIdentity ReadIdentity( + IdentitySearchFactor searchFactor, + string searchFactorValue, + MembershipQuery queryMembership) { - return _identityManagementService2.ReadIdentity( - (TeamFoundation.Framework.Common.IdentitySearchFactor)searchFactor, - searchFactorValue, - MembershipQuery.None, - ReadIdentityOptions.IncludeReadFromSource) - .AsProxy(); + if (string.IsNullOrEmpty(searchFactorValue)) throw new ArgumentException("Value cannot be null or empty.", nameof(searchFactorValue)); + + // TODO: Use configuration options from IWorkItemStore to control proxy creation + return _identityManagementService2.ReadIdentity( + (TeamFoundation.Framework.Common.IdentitySearchFactor)searchFactor, + searchFactorValue, + (TeamFoundation.Framework.Common.MembershipQuery)queryMembership, + ReadIdentityOptions.IncludeReadFromSource) + .AsProxy(); } } } \ No newline at end of file diff --git a/src/Qwiq.Identity.Soap/Qwiq.Identity.Soap.csproj b/src/Qwiq.Identity.Soap/Qwiq.Identity.Soap.csproj index 45af30c5..adceb75d 100644 --- a/src/Qwiq.Identity.Soap/Qwiq.Identity.Soap.csproj +++ b/src/Qwiq.Identity.Soap/Qwiq.Identity.Soap.csproj @@ -1,6 +1,6 @@  - + @@ -201,9 +201,9 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + diff --git a/src/Qwiq.Identity.Soap/packages.config b/src/Qwiq.Identity.Soap/packages.config index c0ae2533..dcee6007 100644 --- a/src/Qwiq.Identity.Soap/packages.config +++ b/src/Qwiq.Identity.Soap/packages.config @@ -4,7 +4,7 @@ - + diff --git a/src/Qwiq.Identity/DisplayNameToAliasValueConverter.cs b/src/Qwiq.Identity/DisplayNameToAliasValueConverter.cs index 7efce19e..c8fb33af 100644 --- a/src/Qwiq.Identity/DisplayNameToAliasValueConverter.cs +++ b/src/Qwiq.Identity/DisplayNameToAliasValueConverter.cs @@ -1,13 +1,10 @@ -using System; +using JetBrains.Annotations; +using Microsoft.VisualStudio.Services.Common; +using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.Contracts; using System.Linq; -using JetBrains.Annotations; - -using Microsoft.VisualStudio.Services.Common; - namespace Microsoft.Qwiq.Identity { /// @@ -36,30 +33,6 @@ public override IReadOnlyDictionary Map(IEnumerable valu return GetIdentityNames(values.ToArray()); } - - - private Dictionary GetIdentityNames(params string[] displayNames) - { - return - GetAliasesForDisplayNames(displayNames) - .ToDictionary( - kvp => kvp.Key, - kvp => - { - if (kvp.Value == null) return null; - var retval = kvp.Value.FirstOrDefault(); - if (kvp.Value.Length > 1) - { - var m = - $"Multiple identities found matching '{kvp.Key}'. Please specify one of the following identities:{string.Join("\r\n- ", kvp.Value)}"; - - throw new MultipleIdentitiesFoundException(m); - } - return (object)retval; - }, - Comparer.OrdinalIgnoreCase); - } - private IDictionary GetAliasesForDisplayNames(string[] displayNames) { if (displayNames == null) throw new ArgumentNullException(nameof(displayNames)); @@ -76,5 +49,23 @@ private IDictionary GetAliasesForDisplayNames(string[] display .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray()); } + + private Dictionary GetIdentityNames(params string[] displayNames) + { + return + GetAliasesForDisplayNames(displayNames) + .ToDictionary( + kvp => kvp.Key, + kvp => + { + if (kvp.Value == null || kvp.Value.Length == 0) return null; + if (kvp.Value.Length > 1) + { + throw new MultipleIdentitiesFoundException(kvp.Key, kvp.Value); + } + return (object)kvp.Value[0]; + }, + Comparer.OrdinalIgnoreCase); + } } -} +} \ No newline at end of file diff --git a/src/Qwiq.Identity/IIdentityManagementService.cs b/src/Qwiq.Identity/IIdentityManagementService.cs index 6d2d46d0..1e0f5bd8 100644 --- a/src/Qwiq.Identity/IIdentityManagementService.cs +++ b/src/Qwiq.Identity/IIdentityManagementService.cs @@ -19,6 +19,16 @@ public interface IIdentityManagementService [Pure] IEnumerable ReadIdentities([NotNull] IEnumerable descriptors); + /// + /// Read identities for given . + /// + /// A set of s + /// + /// + [NotNull] + [Pure] + IEnumerable ReadIdentities([NotNull] IEnumerable descriptors, MembershipQuery queryMembership); + /// /// Read identities for given and . /// @@ -31,8 +41,26 @@ IEnumerable>> ReadIden IdentitySearchFactor searchFactor, [NotNull] IEnumerable searchFactorValues); + /// + /// Read identities for given and . + /// + /// Specific search. + /// Actual search strings. + /// + /// An enumerable set of identities corresponding 1 to 1 with . + [NotNull] + [Pure] + IEnumerable>> ReadIdentities( + IdentitySearchFactor searchFactor, + [NotNull] IEnumerable searchFactorValues, + MembershipQuery queryMembership); + [CanBeNull] [Pure] ITeamFoundationIdentity ReadIdentity(IdentitySearchFactor searchFactor, [NotNull] string searchFactorValue); + + [CanBeNull] + [Pure] + ITeamFoundationIdentity ReadIdentity(IdentitySearchFactor searchFactor, [NotNull] string searchFactorValue, MembershipQuery queryMembership); } } \ No newline at end of file diff --git a/src/Qwiq.Identity/IIdentityValueConverter.cs b/src/Qwiq.Identity/IIdentityValueConverter.cs index 1f3a33e3..706226a5 100644 --- a/src/Qwiq.Identity/IIdentityValueConverter.cs +++ b/src/Qwiq.Identity/IIdentityValueConverter.cs @@ -1,4 +1,3 @@ -using System; using JetBrains.Annotations; using System.Collections.Generic; @@ -17,11 +16,6 @@ public interface IIdentityValueConverter [ContractAnnotation("null => null; notnull => notnull")] U Map([CanBeNull] T value); - IReadOnlyDictionary Map(IEnumerable values); - - [Obsolete("This method is depreciated and will be removed in a future version.")] - object Map(object value); + IReadOnlyDictionary Map(IEnumerable values); } - - -} +} \ No newline at end of file diff --git a/src/Qwiq.Identity/IdentityAliasValueConverter.cs b/src/Qwiq.Identity/IdentityAliasValueConverter.cs index 31da0f28..5d43e5ff 100644 --- a/src/Qwiq.Identity/IdentityAliasValueConverter.cs +++ b/src/Qwiq.Identity/IdentityAliasValueConverter.cs @@ -67,18 +67,6 @@ public override IReadOnlyDictionary Map(IEnumerable valu return retval; } - [Obsolete("This method is depreciated and will be removed in a future version.")] - public override object Map(object value) - { - if (value is string stringValue) return Map(stringValue); - if (value is IEnumerable stringArray) - { - return Map(stringArray).Select(s => (string)s.Value).ToArray(); - } - - return value; - } - private Dictionary GetIdentityForAliases( ICollection logonNames, string tenantId, diff --git a/src/Qwiq.Identity/IdentityValueConverterBase.cs b/src/Qwiq.Identity/IdentityValueConverterBase.cs index fe10d74f..5a18ca8e 100644 --- a/src/Qwiq.Identity/IdentityValueConverterBase.cs +++ b/src/Qwiq.Identity/IdentityValueConverterBase.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; namespace Microsoft.Qwiq.Identity @@ -26,17 +25,5 @@ public virtual object Map(string value) return value; } public abstract IReadOnlyDictionary Map(IEnumerable values); - - [Obsolete("This method is depreciated and will be removed in a future version.")] - public virtual object Map(object value) - { - if (value is string stringValue) return Map(stringValue); - if (value is IEnumerable stringArray) - { - return Map(stringArray).Select(s=>s.Value).ToArray(); - } - - return value; - } } } \ No newline at end of file diff --git a/src/Qwiq.Identity/MembershipQuery.cs b/src/Qwiq.Identity/MembershipQuery.cs new file mode 100644 index 00000000..fb5e1990 --- /dev/null +++ b/src/Qwiq.Identity/MembershipQuery.cs @@ -0,0 +1,29 @@ +namespace Microsoft.Qwiq.Identity +{ + /// + /// Indicates the method to populate + /// + public enum MembershipQuery + { + /// + /// Query will not return any membership data + /// + None = 0, + /// + /// Query will return only direct membership data + /// + Direct = 1, + /// + /// Query will return expanded membership data + /// + Expanded = 2, + /// + /// Query will return expanded up membership data (parents only) + /// + ExpandedUp = 3, + /// + /// Query will return expanded down membership data (children only) + /// + ExpandedDown = 4 + } +} \ No newline at end of file diff --git a/src/Qwiq.Identity/MultipleIdentitiesFoundException.cs b/src/Qwiq.Identity/MultipleIdentitiesFoundException.cs index f6835744..633cfc34 100644 --- a/src/Qwiq.Identity/MultipleIdentitiesFoundException.cs +++ b/src/Qwiq.Identity/MultipleIdentitiesFoundException.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Microsoft.Qwiq.Identity @@ -6,6 +7,11 @@ namespace Microsoft.Qwiq.Identity [Serializable] public class MultipleIdentitiesFoundException : ApplicationException { + public MultipleIdentitiesFoundException(string displayName, IEnumerable matches) + : this($"Multiple identities found matching '{displayName}'. Please specify one of the following identities:\r\n- {string.Join("\r\n- ", matches)}") + { + } + public MultipleIdentitiesFoundException(string message) : base(message) { diff --git a/src/Qwiq.Identity/Qwiq.Identity.csproj b/src/Qwiq.Identity/Qwiq.Identity.csproj index 410b807f..1273d99d 100644 --- a/src/Qwiq.Identity/Qwiq.Identity.csproj +++ b/src/Qwiq.Identity/Qwiq.Identity.csproj @@ -1,6 +1,6 @@  - + @@ -37,6 +37,7 @@ + @@ -82,8 +83,8 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + \ No newline at end of file diff --git a/src/Qwiq.Identity/packages.config b/src/Qwiq.Identity/packages.config index 7ef097d6..a5933dbb 100644 --- a/src/Qwiq.Identity/packages.config +++ b/src/Qwiq.Identity/packages.config @@ -2,7 +2,7 @@ - + diff --git a/src/Qwiq.Linq.Identity/Qwiq.Linq.Identity.csproj b/src/Qwiq.Linq.Identity/Qwiq.Linq.Identity.csproj index 0445005c..5d4789da 100644 --- a/src/Qwiq.Linq.Identity/Qwiq.Linq.Identity.csproj +++ b/src/Qwiq.Linq.Identity/Qwiq.Linq.Identity.csproj @@ -1,6 +1,6 @@  - + @@ -50,8 +50,8 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + \ No newline at end of file diff --git a/src/Qwiq.Linq.Identity/packages.config b/src/Qwiq.Linq.Identity/packages.config index 132b990a..57d0e855 100644 --- a/src/Qwiq.Linq.Identity/packages.config +++ b/src/Qwiq.Linq.Identity/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/src/Qwiq.Linq/Query.cs b/src/Qwiq.Linq/Query.cs index 5193ffe0..475added 100644 --- a/src/Qwiq.Linq/Query.cs +++ b/src/Qwiq.Linq/Query.cs @@ -43,7 +43,6 @@ public Query([NotNull] IQueryProvider provider, [NotNull] IWiqlQueryBuilder buil _provider = provider ?? throw new ArgumentNullException(nameof(provider)); _builder = builder ?? throw new ArgumentNullException(nameof(builder)); _expression = expression ?? throw new ArgumentNullException(nameof(expression)); - ; } Type IQueryable.ElementType => typeof(T); diff --git a/src/Qwiq.Linq/QueryExtensions.cs b/src/Qwiq.Linq/QueryExtensions.cs index c51b7c45..9b7b0467 100644 --- a/src/Qwiq.Linq/QueryExtensions.cs +++ b/src/Qwiq.Linq/QueryExtensions.cs @@ -23,6 +23,16 @@ public static bool WasEver(this T _, T __) { return true; } + + public static bool InGroup(this T _, T __) + { + return true; + } + + public static bool NotInGroup(this T _, T __) + { + return true; + } } } diff --git a/src/Qwiq.Linq/Qwiq.Linq.csproj b/src/Qwiq.Linq/Qwiq.Linq.csproj index 0865e6fa..44bc4b36 100644 --- a/src/Qwiq.Linq/Qwiq.Linq.csproj +++ b/src/Qwiq.Linq/Qwiq.Linq.csproj @@ -1,6 +1,6 @@  - + @@ -54,7 +54,6 @@ - @@ -63,10 +62,12 @@ + + @@ -88,8 +89,8 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + \ No newline at end of file diff --git a/src/Qwiq.Linq/Visitors/LocalCollectionExpander.cs b/src/Qwiq.Linq/Visitors/LocalCollectionExpander.cs deleted file mode 100644 index 9591ec8d..00000000 --- a/src/Qwiq.Linq/Visitors/LocalCollectionExpander.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; - -namespace Microsoft.Qwiq.Linq.Visitors -{ - /// - /// Enables cache key support for local collection values. - /// - [Obsolete("This type has been deprecated and will be removed in a future version.")] - public class LocalCollectionExpander : ExpressionVisitor - { - public static Expression Rewrite(Expression expression) - { - return new LocalCollectionExpander().Visit(expression); - } - - protected override Expression VisitMethodCall(MethodCallExpression node) - { - // pair the method's parameter types with its arguments - var map = node.Method.GetParameters().Zip(node.Arguments, (p, a) => new { Param = p.ParameterType, Arg = a }).ToLinkedList(); - - // deal with instance methods - var instanceType = node.Object == null ? null : node.Object.Type; - map.AddFirst(new { Param = instanceType, Arg = node.Object }); - - // for any local collection parameters in the method, make a - // replacement argument which will print its elements - var replacements = (from x in map - where x.Param != null && x.Param.IsGenericType - let g = x.Param.GetGenericTypeDefinition() - where g == typeof(IEnumerable<>) || g == typeof(List<>) - where x.Arg.NodeType == ExpressionType.Constant - let elementType = x.Param.GetGenericArguments().Single() - let printer = MakePrinter((ConstantExpression)x.Arg, elementType) - select new { x.Arg, Replacement = printer }).ToList(); - - if (replacements.Any()) - { - var args = map.Select( - x => (from r in replacements - where r.Arg == x.Arg - select r.Replacement).SingleOrDefault() - ?? x.Arg) - .ToList(); - - node = node.Update(args.First(), args.Skip(1)); - } - - return base.VisitMethodCall(node); - } - - private ConstantExpression MakePrinter(ConstantExpression enumerable, Type elementType) - { - var value = (IEnumerable)enumerable.Value; - var printerType = typeof(Printer<>).MakeGenericType(elementType); - var printer = Activator.CreateInstance(printerType, value); - - return Expression.Constant(printer); - } - - /// - /// Overrides ToString to print each element of a collection. - /// - /// - /// Inherits List in order to support List.Contains instance method as well - /// as standard Enumerable.Contains/Any extension methods. - /// - private class Printer : List - { - public Printer(IEnumerable collection) - { - AddRange(collection.Cast()); - } - - public override string ToString() - { - return "{" + this.ToConcatenatedString(t => t.ToString(), "|") + "}"; - } - } - } -} \ No newline at end of file diff --git a/src/Qwiq.Linq/Visitors/PartialEvaluator.cs b/src/Qwiq.Linq/Visitors/PartialEvaluator.cs index e6ee5c5d..60b91b40 100644 --- a/src/Qwiq.Linq/Visitors/PartialEvaluator.cs +++ b/src/Qwiq.Linq/Visitors/PartialEvaluator.cs @@ -14,7 +14,7 @@ namespace Microsoft.Qwiq.Linq.Visitors public class PartialEvaluator : ExpressionVisitor { /// - /// Performs evaluation & replacement of independent sub-trees + /// Performs evaluation and replacement of independent sub-trees /// /// The root of the expression tree. /// @@ -28,7 +28,7 @@ public Expression Visit(Expression expression, Func fnCanBeEva } /// - /// Performs evaluation & replacement of independent sub-trees + /// Performs evaluation and replacement of independent sub-trees /// /// The root of the expression tree. /// A new tree with sub-trees evaluated and replaced. @@ -59,19 +59,19 @@ internal Nominator(Func fnCanBeEvaluated) this.fnCanBeEvaluated = fnCanBeEvaluated; } - public override Expression Visit(Expression expression) + public override Expression Visit(Expression node) { - if (expression != null) + if (node != null) { var saveCannotBeEvaluated = cannotBeEvaluated; cannotBeEvaluated = false; - base.Visit(expression); + base.Visit(node); if (!cannotBeEvaluated) - if (fnCanBeEvaluated(expression)) candidates.Add(expression); + if (fnCanBeEvaluated(node)) candidates.Add(node); else cannotBeEvaluated = true; cannotBeEvaluated |= saveCannotBeEvaluated; } - return expression; + return node; } internal HashSet Nominate(Expression expression) @@ -83,7 +83,7 @@ internal HashSet Nominate(Expression expression) } /// - /// Evaluates & replaces sub-trees when first candidate is reached (top-down) + /// Evaluates and replaces sub-trees when first candidate is reached (top-down) /// private class SubtreeEvaluator : ExpressionVisitor { @@ -94,11 +94,11 @@ internal SubtreeEvaluator(HashSet candidates) this.candidates = candidates; } - public override Expression Visit(Expression exp) + public override Expression Visit(Expression node) { - if (exp == null) return null; - if (candidates.Contains(exp)) return Evaluate(exp); - return base.Visit(exp); + if (node == null) return null; + if (candidates.Contains(node)) return Evaluate(node); + return base.Visit(node); } internal Expression Eval(Expression exp) diff --git a/src/Qwiq.Linq/Visitors/QueryRewriter.cs b/src/Qwiq.Linq/Visitors/QueryRewriter.cs index 3053c865..3b2e0d67 100644 --- a/src/Qwiq.Linq/Visitors/QueryRewriter.cs +++ b/src/Qwiq.Linq/Visitors/QueryRewriter.cs @@ -79,6 +79,22 @@ protected override Expression VisitMethodCall(MethodCallExpression node) return new WasEverExpression(node.Type, subject, target); } + if (node.Method.DeclaringType == typeof(QueryExtensions) && node.Method.Name == "InGroup") + { + var subject = Visit(node.Arguments[0]); + var target = Visit(node.Arguments[1]); + + return new InGroupExpression(node.Type, subject, target); + } + + if (node.Method.DeclaringType == typeof(QueryExtensions) && node.Method.Name == "NotInGroup") + { + var subject = Visit(node.Arguments[0]); + var target = Visit(node.Arguments[1]); + + return new NotInGroupExpression(node.Type, subject, target); + } + // This is a contains used to see if a value is in a list, such as: bug => aliases.Contains(bug.AssignedTo) if (node.Method.DeclaringType == typeof(Enumerable) && node.Method.Name == "Contains") { diff --git a/src/Qwiq.Linq/WiqlExpressions/InGroupExpression.cs b/src/Qwiq.Linq/WiqlExpressions/InGroupExpression.cs new file mode 100644 index 00000000..de68b3fe --- /dev/null +++ b/src/Qwiq.Linq/WiqlExpressions/InGroupExpression.cs @@ -0,0 +1,23 @@ +using System; +using System.Linq.Expressions; + +namespace Microsoft.Qwiq.Linq.WiqlExpressions +{ + public class InGroupExpression : Expression + { + internal InGroupExpression(Type type, Expression subject, Expression target) + { + Type = type; + Subject = subject; + Target = target; + } + + public override ExpressionType NodeType => (ExpressionType)WiqlExpressionType.InGroup; + + public override Type Type { get; } + + internal Expression Subject { get; private set; } + internal Expression Target { get; private set; } + } +} + diff --git a/src/Qwiq.Linq/WiqlExpressions/NotInGroupExpression.cs b/src/Qwiq.Linq/WiqlExpressions/NotInGroupExpression.cs new file mode 100644 index 00000000..126671c3 --- /dev/null +++ b/src/Qwiq.Linq/WiqlExpressions/NotInGroupExpression.cs @@ -0,0 +1,23 @@ +using System; +using System.Linq.Expressions; + +namespace Microsoft.Qwiq.Linq.WiqlExpressions +{ + public class NotInGroupExpression : Expression + { + internal NotInGroupExpression(Type type, Expression subject, Expression target) + { + Type = type; + Subject = subject; + Target = target; + } + + public override ExpressionType NodeType => (ExpressionType)WiqlExpressionType.NotInGroup; + + public override Type Type { get; } + + internal Expression Subject { get; private set; } + internal Expression Target { get; private set; } + } +} + diff --git a/src/Qwiq.Linq/WiqlExpressions/WiqlExpressionType.cs b/src/Qwiq.Linq/WiqlExpressions/WiqlExpressionType.cs index 959cc673..b11a9723 100644 --- a/src/Qwiq.Linq/WiqlExpressions/WiqlExpressionType.cs +++ b/src/Qwiq.Linq/WiqlExpressions/WiqlExpressionType.cs @@ -10,7 +10,9 @@ internal enum WiqlExpressionType Contains, Select, Indexer, - WasEver + WasEver, + InGroup, + NotInGroup } } diff --git a/src/Qwiq.Linq/WiqlTranslator.cs b/src/Qwiq.Linq/WiqlTranslator.cs index dfaec170..fa0ba800 100644 --- a/src/Qwiq.Linq/WiqlTranslator.cs +++ b/src/Qwiq.Linq/WiqlTranslator.cs @@ -78,35 +78,39 @@ public Translator(IFieldMapper fieldMapper) _expressionInProgress = new Queue(); } - public override Expression Visit(Expression expression) + public override Expression Visit(Expression node) { - if (expression == null) + if (node == null) { return null; } - switch ((WiqlExpressionType)expression.NodeType) + switch ((WiqlExpressionType)node.NodeType) { case WiqlExpressionType.Select: - return VisitSelect((SelectExpression)expression); + return VisitSelect((SelectExpression)node); case WiqlExpressionType.Where: - return VisitWhere((WhereExpression)expression); + return VisitWhere((WhereExpression)node); case WiqlExpressionType.In: - return VisitIn((InExpression)expression); + return VisitIn((InExpression)node); case WiqlExpressionType.Under: - return VisitUnder((UnderExpression)expression); + return VisitUnder((UnderExpression)node); case WiqlExpressionType.Order: - return VisitOrder((OrderExpression)expression); + return VisitOrder((OrderExpression)node); case WiqlExpressionType.AsOf: - return VisitAsOf((AsOfExpression)expression); + return VisitAsOf((AsOfExpression)node); case WiqlExpressionType.Contains: - return VisitContains((ContainsExpression)expression); + return VisitContains((ContainsExpression)node); case WiqlExpressionType.Indexer: - return VisitIndexer((IndexerExpression) expression); + return VisitIndexer((IndexerExpression) node); case WiqlExpressionType.WasEver: - return VisitWasEver((WasEverExpression)expression); + return VisitWasEver((WasEverExpression)node); + case WiqlExpressionType.InGroup: + return VisitInGroup((InGroupExpression)node); + case WiqlExpressionType.NotInGroup: + return VisitNotInGroup((NotInGroupExpression)node); default: - return base.Visit(expression); + return base.Visit(node); } } @@ -176,6 +180,32 @@ protected virtual Expression VisitWasEver(WasEverExpression expression) return expression; } + protected virtual Expression VisitInGroup(InGroupExpression expression) + { + _expressionInProgress.Enqueue(new GroupStartFragment()); + Visit(expression.Subject); + + _expressionInProgress.Enqueue(new StringFragment(" IN GROUP ")); + + Visit(expression.Target); + _expressionInProgress.Enqueue(new GroupEndFragment()); + + return expression; + } + + protected virtual Expression VisitNotInGroup(NotInGroupExpression expression) + { + _expressionInProgress.Enqueue(new GroupStartFragment()); + Visit(expression.Subject); + + _expressionInProgress.Enqueue(new StringFragment(" NOT IN GROUP ")); + + Visit(expression.Target); + _expressionInProgress.Enqueue(new GroupEndFragment()); + + return expression; + } + protected virtual Expression VisitOrder(OrderExpression expression) { Visit(expression.Source); diff --git a/src/Qwiq.Linq/packages.config b/src/Qwiq.Linq/packages.config index 132b990a..57d0e855 100644 --- a/src/Qwiq.Linq/packages.config +++ b/src/Qwiq.Linq/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/src/Qwiq.Mapper.Identity/BulkIdentityAwareAttributeMapperStrategy.cs b/src/Qwiq.Mapper.Identity/BulkIdentityAwareAttributeMapperStrategy.cs index 64cdd981..e365ff07 100644 --- a/src/Qwiq.Mapper.Identity/BulkIdentityAwareAttributeMapperStrategy.cs +++ b/src/Qwiq.Mapper.Identity/BulkIdentityAwareAttributeMapperStrategy.cs @@ -59,14 +59,14 @@ IIdentityValueConverter identityValueConverter /// /// Maps the specified target work item type. /// - /// Type of the targe work item. + /// Type of the targe work item. /// The work item mappings. /// The work item mapper. - public override void Map(Type targeWorkItemType, IDictionary> workItemMappings, IWorkItemMapper workItemMapper) + public override void Map(Type targetWorkItemType, IDictionary> workItemMappings, IWorkItemMapper workItemMapper) { if (!workItemMappings.Any()) return; - var validIdentityProperties = GetWorkItemIdentityFieldNameToIdentityPropertyMap(targeWorkItemType, _inspector); + var validIdentityProperties = GetWorkItemIdentityFieldNameToIdentityPropertyMap(targetWorkItemType, _inspector); if (!validIdentityProperties.Any()) return; var validIdentityFieldsWithWorkItems = GetWorkItemsWithIdentityFieldValues(workItemMappings.Keys, validIdentityProperties.Keys); @@ -83,7 +83,7 @@ public override void Map(Type targeWorkItemType, IDictionary - + @@ -68,8 +68,8 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + \ No newline at end of file diff --git a/src/Qwiq.Mapper.Identity/packages.config b/src/Qwiq.Mapper.Identity/packages.config index 974e09d8..e0105f61 100644 --- a/src/Qwiq.Mapper.Identity/packages.config +++ b/src/Qwiq.Mapper.Identity/packages.config @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/src/Qwiq.Mapper/AnnotatedPropertyValidator.cs b/src/Qwiq.Mapper/AnnotatedPropertyValidator.cs new file mode 100644 index 00000000..a9f17364 --- /dev/null +++ b/src/Qwiq.Mapper/AnnotatedPropertyValidator.cs @@ -0,0 +1,81 @@ +using JetBrains.Annotations; +using Microsoft.Qwiq.Mapper.Attributes; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.Qwiq.Mapper +{ + public class AnnotatedPropertyValidator : IAnnotatedPropertyValidator + { + private static readonly Type AttributeType = typeof(FieldDefinitionAttribute); + private static readonly ConcurrentDictionary, Dictionary> PropertiesThatExistOnWorkItem = new ConcurrentDictionary, Dictionary>(); + private static readonly ConcurrentDictionary PropertyInfoFields = new ConcurrentDictionary(); + private readonly IPropertyInspector _inspector; + private Func _propertyInfoValidator; + + public AnnotatedPropertyValidator([NotNull] IPropertyInspector inspector) + { + _inspector = inspector ?? throw new ArgumentNullException(nameof(inspector)); + PropertyInfoValidator = (item, info) => + { + var name = GetFieldDefinition(info)?.FieldName; + var validName = !string.IsNullOrWhiteSpace(name); + return validName && item.Fields.Contains(name); + }; + } + + public Func PropertyInfoValidator + { + get => _propertyInfoValidator; + set + { + _propertyInfoValidator = value; + PropertiesThatExistOnWorkItem.Clear(); + } + } + + public FieldDefinitionAttribute GetFieldDefinition(PropertyInfo property) + { + if (property == null) throw new ArgumentNullException(nameof(property)); + return PropertyInfoFields.GetOrAdd( + property, + info => _inspector.GetAttribute(property)); + } + + /// is null. + public IEnumerable> GetValidAnnotatedProperties(IWorkItem workItem, Type targetType) + { + if (PropertyInfoValidator == null) + throw new InvalidOperationException($"{nameof(PropertyInfoValidator)} cannot be null."); + + var workItemTypeName = workItem.WorkItemType; + var key = new Tuple(workItemTypeName, targetType.TypeHandle); + + Dictionary ValueFactory(Tuple tuple) + { + var props = _inspector.GetAnnotatedProperties(targetType, AttributeType); + var retval = new Dictionary(); + foreach (var prop in props) + { + if (prop == null) continue; + var attribute = GetFieldDefinition(prop); + if (attribute == null) continue; + + if (PropertyInfoValidator(workItem, prop)) + { + retval.Add(prop, attribute); + } + } + + return retval; + } + + return PropertiesThatExistOnWorkItem.GetOrAdd( + key, + ValueFactory + ); + } + } +} \ No newline at end of file diff --git a/src/Qwiq.Mapper/Attributes/AttributeMapperStrategy.cs b/src/Qwiq.Mapper/Attributes/AttributeMapperStrategy.cs index cc982de4..736a70fe 100644 --- a/src/Qwiq.Mapper/Attributes/AttributeMapperStrategy.cs +++ b/src/Qwiq.Mapper/Attributes/AttributeMapperStrategy.cs @@ -1,31 +1,61 @@ using FastMember; using JetBrains.Annotations; using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using System.Reflection; namespace Microsoft.Qwiq.Mapper.Attributes { public class AttributeMapperStrategy : WorkItemMapperStrategyBase { - private static readonly ConcurrentDictionary, List> PropertiesThatExistOnWorkItem = new ConcurrentDictionary, List>(); - private static readonly ConcurrentDictionary PropertyInfoFields = new ConcurrentDictionary(); - [NotNull] private readonly IPropertyInspector _inspector; + [NotNull] private readonly IAnnotatedPropertyValidator _annotatedPropertyValidator; [NotNull] private readonly ITypeParser _typeParser; + /// + /// Creates a default instance of with . + /// + public AttributeMapperStrategy() + : this(new PropertyReflector()) + { + } + + /// + /// Creates a new instance of with the specified . + /// + /// An instance of . + public AttributeMapperStrategy([NotNull] IPropertyReflector propertyReflector) + : this(new PropertyInspector(propertyReflector)) + { + } + /// + /// Creates a new instance of with the specified and a default instance of . + /// + /// An instance of . public AttributeMapperStrategy([NotNull] IPropertyInspector inspector) : this(inspector, TypeParser.Default) { } + /// + /// Creates a new instance of with a using the specified and . + /// + /// An instance of . + /// An instance of . public AttributeMapperStrategy([NotNull] IPropertyInspector inspector, [NotNull] ITypeParser typeParser) + : this(new AnnotatedPropertyValidator(inspector), typeParser) + { + } + + /// + /// Creates a new instance of with the specified and . + /// + /// An instance of . + /// An instance of . + public AttributeMapperStrategy([NotNull] IAnnotatedPropertyValidator annotatedPropertyValidator, [NotNull] ITypeParser typeParser) { _typeParser = typeParser ?? throw new ArgumentNullException(nameof(typeParser)); - _inspector = inspector ?? throw new ArgumentNullException(nameof(inspector)); + _annotatedPropertyValidator = annotatedPropertyValidator ?? throw new ArgumentNullException(nameof(annotatedPropertyValidator)); } public override void Map(IDictionary workItemMappings, IWorkItemMapper workItemMapper) @@ -117,68 +147,40 @@ protected internal virtual void AssignFieldValue( } } + protected internal virtual object GetFieldValue(Type targetWorkItemType, IWorkItem sourceWorkItem, string fieldName, PropertyInfo property) + { + object fieldValue; + try + { + fieldValue = sourceWorkItem[fieldName]; + } + catch (DeniedOrNotExistException e) + { + var tm = new TypePair(sourceWorkItem, targetWorkItemType); + var pm = new PropertyMap(property, fieldName); + var message = $"Unable to get field value on {sourceWorkItem.Id}."; + throw new AttributeMapException(message, e, tm, pm); + } + return fieldValue; + } + protected internal virtual void MapImpl(Type targetWorkItemType, IWorkItem sourceWorkItem, object targetWorkItem) { - var properties = PropertiesOnWorkItemCache( - _inspector, - sourceWorkItem, - targetWorkItemType, - typeof(FieldDefinitionAttribute)); + var validAnnotatedPropertyKeyPairs = _annotatedPropertyValidator.GetValidAnnotatedProperties(sourceWorkItem, targetWorkItemType); - foreach (var property in properties) + foreach (var pair in validAnnotatedPropertyKeyPairs) { - var a = PropertyInfoFieldCache(_inspector, property); - if (a == null) continue; + var fieldDefinitionAttribute = pair.Value; + if (fieldDefinitionAttribute == null) continue; - var fieldName = a.FieldName; - var convert = a.RequireConversion; - var nullSub = a.NullSubstitute; - object fieldValue; - try - { - fieldValue = sourceWorkItem[fieldName]; - } - catch (DeniedOrNotExistException e) - { - var tm = new TypePair(sourceWorkItem, targetWorkItemType); - var pm = new PropertyMap(property, fieldName); - var message = $"Unable to get field value on {sourceWorkItem.Id}."; - throw new AttributeMapException(message, e, tm, pm); - } + var property = pair.Key; + var fieldName = fieldDefinitionAttribute.FieldName; + var convert = fieldDefinitionAttribute.RequireConversion; + var nullSub = fieldDefinitionAttribute.NullSubstitute; + var fieldValue = GetFieldValue(targetWorkItemType, sourceWorkItem, fieldName, property); AssignFieldValue(targetWorkItemType, sourceWorkItem, targetWorkItem, property, fieldName, convert, nullSub, fieldValue); } } - - private static IEnumerable PropertiesOnWorkItemCache(IPropertyInspector inspector, IWorkItem workItem, Type targetType, Type attributeType) - { - // Composite key: work item type and target type - - var workItemType = workItem.WorkItemType; - var key = new Tuple(workItemType, targetType.TypeHandle); - - return PropertiesThatExistOnWorkItem.GetOrAdd( - key, - tuple => - { - return - inspector.GetAnnotatedProperties(targetType, typeof(FieldDefinitionAttribute)) - .Select( - property => - new { property, fieldName = PropertyInfoFieldCache(inspector, property)?.FieldName }) - .Where( - t => - !string.IsNullOrEmpty(t.fieldName)) - .Select(t => t.property) - .ToList(); - }); - } - - private static FieldDefinitionAttribute PropertyInfoFieldCache(IPropertyInspector inspector, PropertyInfo property) - { - return PropertyInfoFields.GetOrAdd( - property, - info => inspector.GetAttribute(property)); - } } } \ No newline at end of file diff --git a/src/Qwiq.Mapper/Attributes/NoExceptionAttributeMapperStrategy.cs b/src/Qwiq.Mapper/Attributes/NoExceptionAttributeMapperStrategy.cs index c2cb484f..03d0eae2 100644 --- a/src/Qwiq.Mapper/Attributes/NoExceptionAttributeMapperStrategy.cs +++ b/src/Qwiq.Mapper/Attributes/NoExceptionAttributeMapperStrategy.cs @@ -1,18 +1,55 @@ -using System; +using JetBrains.Annotations; +using System; using System.Diagnostics; using System.Reflection; -using JetBrains.Annotations; namespace Microsoft.Qwiq.Mapper.Attributes { public class NoExceptionAttributeMapperStrategy : AttributeMapperStrategy { - public NoExceptionAttributeMapperStrategy([NotNull] IPropertyInspector inspector) : base(inspector) + /// + /// Creates a default instance of with . + /// + public NoExceptionAttributeMapperStrategy() + : base() { } + /// + /// Creates a new instance of with the specified . + /// + /// An instance of . + public NoExceptionAttributeMapperStrategy([NotNull] IPropertyReflector propertyReflector) + : base(propertyReflector) + { + } + + /// + /// Creates a new instance of with the specified and a default instance of . + /// + /// An instance of . + public NoExceptionAttributeMapperStrategy([NotNull] IPropertyInspector inspector) + : base(inspector) + { + } + + /// + /// Creates a new instance of with a using the specified and . + /// + /// An instance of . + /// An instance of . public NoExceptionAttributeMapperStrategy([NotNull] IPropertyInspector inspector, [NotNull] ITypeParser typeParser) - :base(inspector, typeParser) + : base(inspector, typeParser) + { + } + + /// + /// Creates a new instance of with the specified and . + /// + /// An instance of . + /// An instance of . + public NoExceptionAttributeMapperStrategy([NotNull] IAnnotatedPropertyValidator annotatedPropertyValidator, [NotNull] ITypeParser typeParser) + : base(annotatedPropertyValidator, typeParser) { } @@ -37,7 +74,9 @@ protected internal override void AssignFieldValue(Type targetWorkItemType, IWork targetWorkItemType.Name, $"{property.Name} ({property.PropertyType.FullName})"); } +#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body catch (Exception) +#pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body { // Best effort } @@ -54,11 +93,58 @@ protected internal override void AssignFieldValue(Type targetWorkItemType, IWork $"{property.Name} ({property.PropertyType.FullName})", e.Message); } +#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body catch (Exception) +#pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body { // Best effort } } } + + protected internal override object GetFieldValue(Type targetWorkItemType, IWorkItem sourceWorkItem, string fieldName, PropertyInfo property) + { + try + { + return base.GetFieldValue(targetWorkItemType, sourceWorkItem, fieldName, property); + } + catch (DeniedOrNotExistException e) + { + // This is most likely caused by the field not being on the WIT + // Frequently encountered when using a single entity to map different WITs with different sets of fields + + try + { + Trace.TraceWarning(e.Message); + } +#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body + catch (Exception) +#pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body + { + // Best effort + } + } + catch (Exception e) + { + try + { + Trace.TraceWarning( + "Could not map field '{0}' from type '{1}' to type '{2}.{3}'. {4}", + fieldName, + sourceWorkItem.WorkItemType, + targetWorkItemType.Name, + $"{property.Name} ({property.PropertyType.FullName})", + e.Message); + } +#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body + catch (Exception) +#pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body + { + // Best effort + } + } + + return null; + } } -} +} \ No newline at end of file diff --git a/src/Qwiq.Mapper/Attributes/PropertyInspector.cs b/src/Qwiq.Mapper/Attributes/PropertyInspector.cs index ec92999d..1c2d599a 100644 --- a/src/Qwiq.Mapper/Attributes/PropertyInspector.cs +++ b/src/Qwiq.Mapper/Attributes/PropertyInspector.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -8,17 +9,17 @@ namespace Microsoft.Qwiq.Mapper.Attributes { public class PropertyInspector : IPropertyInspector { - private readonly IPropertyReflector _reflector; private static readonly ConcurrentDictionary>> AnnotatedProperties = new ConcurrentDictionary>>(); + private readonly IPropertyReflector _reflector; - public PropertyInspector(IPropertyReflector reflector) + public PropertyInspector([NotNull] IPropertyReflector reflector) { - _reflector = reflector; + _reflector = reflector ?? throw new ArgumentNullException(nameof(reflector)); } public IEnumerable GetAnnotatedProperties(Type workItemType, Type attributeType) { - return AnnotatedPropertiesCache(_reflector, workItemType, attributeType); + return AnnotatedPropertiesCache(_reflector, workItemType, attributeType); } public T GetAttribute(PropertyInfo property) where T : Attribute @@ -62,5 +63,4 @@ private static IEnumerable AnnotatedPropertiesCache(IPropertyRefle return pis2; } } -} - +} \ No newline at end of file diff --git a/src/Qwiq.Mapper/Attributes/WorkItemLinksMapperStrategy.cs b/src/Qwiq.Mapper/Attributes/WorkItemLinksMapperStrategy.cs index e7072378..357da197 100644 --- a/src/Qwiq.Mapper/Attributes/WorkItemLinksMapperStrategy.cs +++ b/src/Qwiq.Mapper/Attributes/WorkItemLinksMapperStrategy.cs @@ -11,16 +11,14 @@ namespace Microsoft.Qwiq.Mapper.Attributes { public class WorkItemLinksMapperStrategy : WorkItemMapperStrategyBase { - private readonly IPropertyInspector _inspector; - - protected IWorkItemStore Store { get; } + private static readonly ConcurrentDictionary, List> + PropertiesThatExistOnWorkItem = + new ConcurrentDictionary, List>(); private static readonly ConcurrentDictionary PropertyInfoFields = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary, List> - PropertiesThatExistOnWorkItem = - new ConcurrentDictionary, List>(); + private readonly IPropertyInspector _inspector; public WorkItemLinksMapperStrategy(IPropertyInspector inspector, IWorkItemStore store) { @@ -28,35 +26,7 @@ public WorkItemLinksMapperStrategy(IPropertyInspector inspector, IWorkItemStore Store = store; } - private static WorkItemLinkAttribute PropertyInfoLinkTypeCache( - IPropertyInspector inspector, - PropertyInfo property) - { - return PropertyInfoFields.GetOrAdd( - property, - info => inspector.GetAttribute(property)); - } - - private static IEnumerable PropertiesOnWorkItemCache( - IPropertyInspector inspector, - IWorkItem workItem, - Type targetType, - Type attributeType) - { - // Composite key: work item type and target type - - var workItemType = workItem.WorkItemType; - var key = new Tuple(workItemType, targetType.TypeHandle); - - return PropertiesThatExistOnWorkItem.GetOrAdd( - key, - tuple => inspector.GetAnnotatedProperties(targetType, attributeType).ToList()); - } - - protected virtual IEnumerable Query(IEnumerable ids) - { - return Store.Query(ids); - } + protected IWorkItemStore Store { get; } public override void Map(Type targetWorkItemType, IDictionary> workItemMappings, IWorkItemMapper workItemMapper) { @@ -75,7 +45,6 @@ public override void Map(Type targetWorkItemType, IDictionary k.Key.Id, e => e); var accessor = TypeAccessor.Create(targetWorkItemType, true); - // REVIEW: The recursion of links can cause mapping multiple times on the same values // For example, a common ancestor var previouslyMapped = new Dictionary, IIdentifiable>(); @@ -111,10 +80,9 @@ public override void Map(Type targetWorkItemType, IDictionary !previouslyMapped.ContainsKey(new Tuple(p, propertyType.TypeHandle))) + .Where(p => !previouslyMapped.ContainsKey(new Tuple(p, propertyType.TypeHandle))) .Select( s => { @@ -162,6 +130,36 @@ public override void Map(Type targetWorkItemType, IDictionary Query(IEnumerable ids) + { + return Store.Query(ids); + } + + private static IEnumerable PropertiesOnWorkItemCache( + IPropertyInspector inspector, + IWorkItem workItem, + Type targetType, + Type attributeType) + { + // Composite key: work item type and target type + + var workItemType = workItem.WorkItemType; + var key = new Tuple(workItemType, targetType.TypeHandle); + + return PropertiesThatExistOnWorkItem.GetOrAdd( + key, + tuple => inspector.GetAnnotatedProperties(targetType, attributeType).ToList()); + } + + private static WorkItemLinkAttribute PropertyInfoLinkTypeCache( + IPropertyInspector inspector, + PropertyInfo property) + { + return PropertyInfoFields.GetOrAdd( + property, + info => inspector.GetAttribute(property)); + } + private Dictionary, List> BuildLinksRelationships( Type targetWorkItemType, IEnumerable>> workItemMappings) @@ -207,4 +205,4 @@ private Dictionary, List> BuildLinksRelationships( return linksLookup; } } -} +} \ No newline at end of file diff --git a/src/Qwiq.Mapper/IAnnotatedPropertyValidator.cs b/src/Qwiq.Mapper/IAnnotatedPropertyValidator.cs new file mode 100644 index 00000000..0e1f8ddb --- /dev/null +++ b/src/Qwiq.Mapper/IAnnotatedPropertyValidator.cs @@ -0,0 +1,36 @@ +using JetBrains.Annotations; +using Microsoft.Qwiq.Mapper.Attributes; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.Qwiq.Mapper +{ + /// + /// Validates annotated properties + /// + public interface IAnnotatedPropertyValidator + { + /// + /// Inspects a and and returns true if the is valid; otherwise, false. + /// + Func PropertyInfoValidator { get; set; } + + /// + /// Gets an instance of for a given . + /// + /// An instance of decorated with . + /// If the is decorated with then the attribute; otherwise, null. + [CanBeNull] + FieldDefinitionAttribute GetFieldDefinition([NotNull] PropertyInfo property); + + /// + /// Gets and validates annotated properties of against . + /// + /// An instance of . + /// The type being mapped. + /// A collection of dedocated with that are valid. + [NotNull] + IEnumerable> GetValidAnnotatedProperties([NotNull] IWorkItem workItem, [NotNull] Type targetType); + } +} \ No newline at end of file diff --git a/src/Qwiq.Mapper/IIdentifiable.cs b/src/Qwiq.Mapper/IIdentifiable.cs deleted file mode 100644 index 3281dc31..00000000 --- a/src/Qwiq.Mapper/IIdentifiable.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Microsoft.Qwiq.Mapper -{ - [Obsolete("This interface has been deprecated and will be removed in a future version. See Microsoft.Qwiq.IIdentifiable`1")] - public interface IIdentifiable : IIdentifiable - { - } -} - diff --git a/src/Qwiq.Mapper/Qwiq.Mapper.csproj b/src/Qwiq.Mapper/Qwiq.Mapper.csproj index 022a1f49..c5214f7a 100644 --- a/src/Qwiq.Mapper/Qwiq.Mapper.csproj +++ b/src/Qwiq.Mapper/Qwiq.Mapper.csproj @@ -1,6 +1,6 @@  - + @@ -28,7 +28,9 @@ Properties\AssemblyInfo.Common.cs + + @@ -40,7 +42,6 @@ - @@ -70,8 +71,8 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + \ No newline at end of file diff --git a/src/Qwiq.Mapper/WorkItemMapper.cs b/src/Qwiq.Mapper/WorkItemMapper.cs index 645dc01f..8a625091 100644 --- a/src/Qwiq.Mapper/WorkItemMapper.cs +++ b/src/Qwiq.Mapper/WorkItemMapper.cs @@ -16,6 +16,16 @@ public class WorkItemMapper : IWorkItemMapper private delegate IIdentifiable ObjectActivator(); private static readonly ConcurrentDictionary OptimizedCtorExpression = new ConcurrentDictionary(); + public WorkItemMapper([NotNull] params IWorkItemMapperStrategy[] mapperStrategies) + { + Contract.Requires(mapperStrategies != null); + + if (mapperStrategies == null) throw new ArgumentNullException(nameof(mapperStrategies)); + if (mapperStrategies.Length == 0) throw new ArgumentException("Value cannot be an empty collection.", nameof(mapperStrategies)); + + MapperStrategies = mapperStrategies; + } + public WorkItemMapper([NotNull] IEnumerable mapperStrategies) { Contract.Requires(mapperStrategies != null); diff --git a/src/Qwiq.Mapper/packages.config b/src/Qwiq.Mapper/packages.config index 974e09d8..e0105f61 100644 --- a/src/Qwiq.Mapper/packages.config +++ b/src/Qwiq.Mapper/packages.config @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/test/Qwiq.Benchmark/Qwiq.Benchmark.csproj b/test/Qwiq.Benchmark/Qwiq.Benchmark.csproj index bf90ad65..7293f1ff 100644 --- a/test/Qwiq.Benchmark/Qwiq.Benchmark.csproj +++ b/test/Qwiq.Benchmark/Qwiq.Benchmark.csproj @@ -1,7 +1,7 @@  - - + + {D9ED32D7-03FA-468B-AD1A-249CEF9C6CDB} @@ -22,46 +22,49 @@ - - ..\..\packages\BenchmarkDotNet.0.10.5\lib\net46\BenchmarkDotNet.dll + + ..\..\packages\BenchmarkDotNet.0.10.11\lib\net46\BenchmarkDotNet.dll - - ..\..\packages\BenchmarkDotNet.Core.0.10.5\lib\net46\BenchmarkDotNet.Core.dll + + ..\..\packages\BenchmarkDotNet.Core.0.10.11\lib\net46\BenchmarkDotNet.Core.dll - - ..\..\packages\BenchmarkDotNet.Diagnostics.Windows.0.10.5\lib\net46\BenchmarkDotNet.Diagnostics.Windows.dll + + ..\..\packages\BenchmarkDotNet.Diagnostics.Windows.0.10.11\lib\net46\BenchmarkDotNet.Diagnostics.Windows.dll - - ..\..\packages\BenchmarkDotNet.Toolchains.Roslyn.0.10.5\lib\net46\BenchmarkDotNet.Toolchains.Roslyn.dll + + ..\..\packages\BenchmarkDotNet.Toolchains.Roslyn.0.10.11\lib\net46\BenchmarkDotNet.Toolchains.Roslyn.dll - - ..\..\packages\Microsoft.CodeAnalysis.Common.2.1.0\lib\netstandard1.3\Microsoft.CodeAnalysis.dll + + ..\..\packages\Microsoft.CodeAnalysis.Common.2.6.1\lib\netstandard1.3\Microsoft.CodeAnalysis.dll - - ..\..\packages\Microsoft.CodeAnalysis.CSharp.2.1.0\lib\netstandard1.3\Microsoft.CodeAnalysis.CSharp.dll + + ..\..\packages\Microsoft.CodeAnalysis.CSharp.2.6.1\lib\netstandard1.3\Microsoft.CodeAnalysis.CSharp.dll + ..\..\packages\Microsoft.Diagnostics.Tracing.TraceEvent.1.0.41\lib\net40\Microsoft.Diagnostics.Tracing.TraceEvent.dll ..\..\packages\Microsoft.DotNet.InternalAbstractions.1.0.0\lib\net451\Microsoft.DotNet.InternalAbstractions.dll - - ..\..\packages\Microsoft.DotNet.PlatformAbstractions.1.1.1\lib\net451\Microsoft.DotNet.PlatformAbstractions.dll + + ..\..\packages\Microsoft.DotNet.PlatformAbstractions.2.0.4\lib\net45\Microsoft.DotNet.PlatformAbstractions.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + ..\..\packages\Microsoft.Win32.Registry.4.4.0\lib\net46\Microsoft.Win32.Registry.dll ..\..\packages\System.AppContext.4.3.0\lib\net46\System.AppContext.dll - - ..\..\packages\System.Collections.Immutable.1.3.1\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll - True + + ..\..\packages\System.Collections.Immutable.1.4.0\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll @@ -84,11 +87,12 @@ - - ..\..\packages\System.Reflection.Metadata.1.4.2\lib\portable-net45+win8\System.Reflection.Metadata.dll + + ..\..\packages\System.Reflection.Metadata.1.5.0\lib\portable-net45+win8\System.Reflection.Metadata.dll - ..\..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net46\System.Security.Cryptography.Algorithms.dll + ..\..\packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net46\System.Security.Cryptography.Algorithms.dll + True ..\..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll @@ -97,24 +101,25 @@ ..\..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll - ..\..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net46\System.Security.Cryptography.X509Certificates.dll + ..\..\packages\System.Security.Cryptography.X509Certificates.4.3.2\lib\net46\System.Security.Cryptography.X509Certificates.dll + True - ..\..\packages\System.Text.Encoding.CodePages.4.3.0\lib\net46\System.Text.Encoding.CodePages.dll + ..\..\packages\System.Text.Encoding.CodePages.4.4.0\lib\net46\System.Text.Encoding.CodePages.dll - - ..\..\packages\System.Threading.Tasks.Extensions.4.3.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll + + ..\..\packages\System.Threading.Tasks.Extensions.4.4.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll ..\..\packages\System.Threading.Thread.4.3.0\lib\net46\System.Threading.Thread.dll - - ..\..\packages\System.ValueTuple.4.3.0\lib\netstandard1.0\System.ValueTuple.dll + + ..\..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll - ..\..\packages\System.Xml.ReaderWriter.4.3.0\lib\net46\System.Xml.ReaderWriter.dll + ..\..\packages\System.Xml.ReaderWriter.4.3.1\lib\net46\System.Xml.ReaderWriter.dll ..\..\packages\System.Xml.XmlDocument.4.3.0\lib\net46\System.Xml.XmlDocument.dll @@ -162,12 +167,12 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - + + + - + \ No newline at end of file diff --git a/test/Qwiq.Benchmark/app.config b/test/Qwiq.Benchmark/app.config index cc65beb9..eff82453 100644 --- a/test/Qwiq.Benchmark/app.config +++ b/test/Qwiq.Benchmark/app.config @@ -12,11 +12,11 @@ - + - + @@ -52,7 +52,7 @@ - + @@ -92,7 +92,7 @@ - + @@ -126,6 +126,18 @@ + + + + + + + + + + + + diff --git a/test/Qwiq.Benchmark/packages.config b/test/Qwiq.Benchmark/packages.config index 0c54ee9a..13d66551 100644 --- a/test/Qwiq.Benchmark/packages.config +++ b/test/Qwiq.Benchmark/packages.config @@ -1,23 +1,24 @@  - - - - + + + + - - + + - - - - + + + + + - + @@ -31,30 +32,31 @@ - - + + - + - + - + - + - + - - + + + \ No newline at end of file diff --git a/test/Qwiq.Core.Tests/Qwiq.Core.UnitTests.csproj b/test/Qwiq.Core.Tests/Qwiq.Core.UnitTests.csproj index 4b3cbd6e..ace2ca2e 100644 --- a/test/Qwiq.Core.Tests/Qwiq.Core.UnitTests.csproj +++ b/test/Qwiq.Core.Tests/Qwiq.Core.UnitTests.csproj @@ -1,7 +1,7 @@  - - + + {57407CCA-8444-4713-95E9-CFC1168D846B} @@ -151,10 +151,10 @@ ..\..\packages\Microsoft.VisualStudio.Services.Client.15.112.1\lib\net45\Microsoft.VisualStudio.Services.WebApi.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll ..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll @@ -256,13 +256,13 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - + + + - + \ No newline at end of file diff --git a/test/Qwiq.Core.Tests/packages.config b/test/Qwiq.Core.Tests/packages.config index fc27df2c..8a353099 100644 --- a/test/Qwiq.Core.Tests/packages.config +++ b/test/Qwiq.Core.Tests/packages.config @@ -4,15 +4,15 @@ - + - - + + diff --git a/test/Qwiq.Identity.Benchmark.Tests/Benchmark.cs b/test/Qwiq.Identity.Benchmark.Tests/Benchmark.cs index 23bbbc3a..03a5fb33 100644 --- a/test/Qwiq.Identity.Benchmark.Tests/Benchmark.cs +++ b/test/Qwiq.Identity.Benchmark.Tests/Benchmark.cs @@ -26,6 +26,9 @@ public override void When() } [TestMethod] +#if DEBUG + [Ignore] +#endif [TestCategory(Constants.TestCategory.Benchmark)] [TestCategory(Constants.TestCategory.Performance)] public void Execute_Identity_Mapping_Performance_Benchmark() @@ -39,7 +42,7 @@ public class Benchmark private IWorkItemMapperStrategy _strategy; private Dictionary> _workItemMappings; - [Setup] + [GlobalSetup] public void SetupData() { var propertyInspector = new PropertyInspector(new PropertyReflector()); diff --git a/test/Qwiq.Identity.Benchmark.Tests/Qwiq.Identity.BenchmarkTests.csproj b/test/Qwiq.Identity.Benchmark.Tests/Qwiq.Identity.BenchmarkTests.csproj index 4ee0022e..69278b94 100644 --- a/test/Qwiq.Identity.Benchmark.Tests/Qwiq.Identity.BenchmarkTests.csproj +++ b/test/Qwiq.Identity.Benchmark.Tests/Qwiq.Identity.BenchmarkTests.csproj @@ -1,7 +1,7 @@  - - + + @@ -23,40 +23,43 @@ - - ..\..\packages\BenchmarkDotNet.0.10.5\lib\net46\BenchmarkDotNet.dll + + ..\..\packages\BenchmarkDotNet.0.10.11\lib\net46\BenchmarkDotNet.dll - - ..\..\packages\BenchmarkDotNet.Core.0.10.5\lib\net46\BenchmarkDotNet.Core.dll + + ..\..\packages\BenchmarkDotNet.Core.0.10.11\lib\net46\BenchmarkDotNet.Core.dll - - ..\..\packages\BenchmarkDotNet.Toolchains.Roslyn.0.10.5\lib\net46\BenchmarkDotNet.Toolchains.Roslyn.dll + + ..\..\packages\BenchmarkDotNet.Toolchains.Roslyn.0.10.11\lib\net46\BenchmarkDotNet.Toolchains.Roslyn.dll - - ..\..\packages\Microsoft.CodeAnalysis.Common.2.1.0\lib\netstandard1.3\Microsoft.CodeAnalysis.dll + + ..\..\packages\Microsoft.CodeAnalysis.Common.2.6.1\lib\netstandard1.3\Microsoft.CodeAnalysis.dll - - ..\..\packages\Microsoft.CodeAnalysis.CSharp.2.1.0\lib\netstandard1.3\Microsoft.CodeAnalysis.CSharp.dll + + ..\..\packages\Microsoft.CodeAnalysis.CSharp.2.6.1\lib\netstandard1.3\Microsoft.CodeAnalysis.CSharp.dll + ..\..\packages\Microsoft.DotNet.InternalAbstractions.1.0.0\lib\net451\Microsoft.DotNet.InternalAbstractions.dll - - ..\..\packages\Microsoft.DotNet.PlatformAbstractions.1.1.1\lib\net451\Microsoft.DotNet.PlatformAbstractions.dll + + ..\..\packages\Microsoft.DotNet.PlatformAbstractions.2.0.4\lib\net45\Microsoft.DotNet.PlatformAbstractions.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + ..\..\packages\Microsoft.Win32.Registry.4.4.0\lib\net46\Microsoft.Win32.Registry.dll ..\..\packages\System.AppContext.4.3.0\lib\net46\System.AppContext.dll - - ..\..\packages\System.Collections.Immutable.1.3.1\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll - True + + ..\..\packages\System.Collections.Immutable.1.4.0\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll @@ -81,11 +84,12 @@ - - ..\..\packages\System.Reflection.Metadata.1.4.2\lib\portable-net45+win8\System.Reflection.Metadata.dll + + ..\..\packages\System.Reflection.Metadata.1.5.0\lib\portable-net45+win8\System.Reflection.Metadata.dll - ..\..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net46\System.Security.Cryptography.Algorithms.dll + ..\..\packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net46\System.Security.Cryptography.Algorithms.dll + True ..\..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll @@ -94,24 +98,25 @@ ..\..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll - ..\..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net46\System.Security.Cryptography.X509Certificates.dll + ..\..\packages\System.Security.Cryptography.X509Certificates.4.3.2\lib\net46\System.Security.Cryptography.X509Certificates.dll + True - ..\..\packages\System.Text.Encoding.CodePages.4.3.0\lib\net46\System.Text.Encoding.CodePages.dll + ..\..\packages\System.Text.Encoding.CodePages.4.4.0\lib\net46\System.Text.Encoding.CodePages.dll - - ..\..\packages\System.Threading.Tasks.Extensions.4.3.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll + + ..\..\packages\System.Threading.Tasks.Extensions.4.4.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll ..\..\packages\System.Threading.Thread.4.3.0\lib\net46\System.Threading.Thread.dll - - ..\..\packages\System.ValueTuple.4.3.0\lib\netstandard1.0\System.ValueTuple.dll + + ..\..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll - ..\..\packages\System.Xml.ReaderWriter.4.3.0\lib\net46\System.Xml.ReaderWriter.dll + ..\..\packages\System.Xml.ReaderWriter.4.3.1\lib\net46\System.Xml.ReaderWriter.dll ..\..\packages\System.Xml.XmlDocument.4.3.0\lib\net46\System.Xml.XmlDocument.dll @@ -182,11 +187,11 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - + + + - + \ No newline at end of file diff --git a/test/Qwiq.Identity.Benchmark.Tests/app.config b/test/Qwiq.Identity.Benchmark.Tests/app.config index 62f2630c..23388106 100644 --- a/test/Qwiq.Identity.Benchmark.Tests/app.config +++ b/test/Qwiq.Identity.Benchmark.Tests/app.config @@ -4,15 +4,15 @@ - + - + - + @@ -40,7 +40,7 @@ - + @@ -68,7 +68,7 @@ - + @@ -86,6 +86,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Qwiq.Identity.Benchmark.Tests/packages.config b/test/Qwiq.Identity.Benchmark.Tests/packages.config index dad4371f..58ce3ddd 100644 --- a/test/Qwiq.Identity.Benchmark.Tests/packages.config +++ b/test/Qwiq.Identity.Benchmark.Tests/packages.config @@ -1,21 +1,22 @@  - - - + + + - - + + - - - - + + + + + - + @@ -29,30 +30,31 @@ - - + + - + - + - + - + - + - - + + + \ No newline at end of file diff --git a/test/Qwiq.Identity.Tests/BulkIdentityAwareAttributeMapperStrategyTests.cs b/test/Qwiq.Identity.Tests/BulkIdentityAwareAttributeMapperStrategyTests.cs index b897c235..f9b95c2d 100644 --- a/test/Qwiq.Identity.Tests/BulkIdentityAwareAttributeMapperStrategyTests.cs +++ b/test/Qwiq.Identity.Tests/BulkIdentityAwareAttributeMapperStrategyTests.cs @@ -11,6 +11,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Should; +using MockIdentityDescriptor = Microsoft.Qwiq.Mocks.MockIdentityDescriptor; namespace Microsoft.Qwiq.Identity { @@ -73,16 +74,14 @@ public void the_actual_identity_value_should_be_null() [TestClass] public class when_the_backing_source_does_result_in_a_resolved_identity : BulkIdentityAwareAttributeMapperStrategyTests { - private const string identityAlias = "jsmit"; - private const string identityDisplay = "Joe Smit"; + private const string IdentityAlias = "jsmit"; + private const string IdentityDisplay = "Joe Smit"; public override void Given() { - IdentityFieldBackingValue = identityDisplay; + IdentityFieldBackingValue = IdentityDisplay; Identities = new Dictionary> { -#pragma warning disable 0618 - {IdentityFieldBackingValue, new []{new MockTeamFoundationIdentity(identityDisplay, identityAlias) }} -#pragma warning restore 0618 + {IdentityFieldBackingValue, new []{new MockTeamFoundationIdentity(MockIdentityDescriptor.Create(IdentityAlias), IdentityDisplay, Guid.Empty) }} }; base.Given(); } @@ -90,7 +89,7 @@ public override void Given() [TestMethod] public void the_actual_identity_value_should_be_the_identity_alias() { - Actual.AnIdentity.ShouldEqual(identityAlias); + Actual.AnIdentity.ShouldEqual(IdentityAlias); } [TestMethod] @@ -103,8 +102,8 @@ public void set_on_an_identity_property_should_be_called_once() public void the_IdentityFieldValue_contains_expected_value() { Actual.AnIdentityValue.ShouldNotBeNull(); - Actual.AnIdentityValue.DisplayName.ShouldEqual(identityDisplay); - Actual.AnIdentityValue.IdentityName.ShouldEqual(identityAlias); + Actual.AnIdentityValue.DisplayName.ShouldEqual(IdentityDisplay); + Actual.AnIdentityValue.IdentityName.ShouldEqual(IdentityAlias); } } diff --git a/test/Qwiq.Identity.Tests/IdentityMapperTests.cs b/test/Qwiq.Identity.Tests/IdentityMapperTests.cs index 1a4fb5c5..c984dc2c 100644 --- a/test/Qwiq.Identity.Tests/IdentityMapperTests.cs +++ b/test/Qwiq.Identity.Tests/IdentityMapperTests.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; - +using System.Linq; using Microsoft.Qwiq.Mocks; using Microsoft.Qwiq.Tests.Common; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -32,13 +32,25 @@ public override void Given() public override void When() { -#pragma warning disable 0618 - var result = Instance.Map(Input); -#pragma warning restore 0618 +#pragma warning disable IDE0019 // Use pattern matching + var stringValue = Input as string; + var stringArray = Input as IEnumerable; +#pragma warning restore IDE0019 // Use pattern matching - Debug.Print("Result: " + result.ToUsefulString()); + if (stringValue != null) + { + ActualOutput = (T)Instance.Map(stringValue); + } + else if (stringArray != null) + { + ActualOutput = (T)Instance.Map(stringArray).Values; + } + else + { + ActualOutput = Input; + } - ActualOutput = (T)result; + Debug.Print("Result: " + ActualOutput.ToUsefulString()); } [TestMethod] @@ -79,6 +91,12 @@ public override void Given() Input = new[] { "alias", "other" }; ExpectedOutput = new[] { "alias@domain", "other@domain" }; } + + public override void When() + { + ActualOutput = Instance.Map(Input).Values.Select(s => s.ToString()); + Debug.Print("Result: " + ActualOutput.ToUsefulString()); + } } [TestClass] @@ -90,6 +108,12 @@ public override void Given() Input = new[] { "alias", "noidentity2" }; ExpectedOutput = new[] { "alias@domain", "noidentity2" }; } + + public override void When() + { + ActualOutput = Instance.Map(Input).Values.Select(s => s.ToString()); + Debug.Print("Result: " + ActualOutput.ToUsefulString()); + } } [TestClass] diff --git a/test/Qwiq.Identity.Tests/Mocks/MockIdentityManagementService2.cs b/test/Qwiq.Identity.Tests/Mocks/MockIdentityManagementService2.cs index 01e40afb..7dfb167f 100644 --- a/test/Qwiq.Identity.Tests/Mocks/MockIdentityManagementService2.cs +++ b/test/Qwiq.Identity.Tests/Mocks/MockIdentityManagementService2.cs @@ -107,7 +107,7 @@ public FilteredIdentitiesList ReadFilteredIdentities( public TeamFoundation.Framework.Client.TeamFoundationIdentity[] ReadIdentities( TeamFoundation.Framework.Client.IdentityDescriptor[] descriptors, - MembershipQuery queryMembership, + TeamFoundation.Framework.Common.MembershipQuery queryMembership, ReadIdentityOptions readOptions) { return NullIdentities; @@ -115,7 +115,7 @@ public TeamFoundation.Framework.Client.TeamFoundationIdentity[] ReadIdentities( public TeamFoundation.Framework.Client.TeamFoundationIdentity[] ReadIdentities( Guid[] teamFoundationIds, - MembershipQuery queryMembership) + TeamFoundation.Framework.Common.MembershipQuery queryMembership) { return NullIdentities; } @@ -123,7 +123,7 @@ public TeamFoundation.Framework.Client.TeamFoundationIdentity[] ReadIdentities( public TeamFoundation.Framework.Client.TeamFoundationIdentity[][] ReadIdentities( TeamFoundation.Framework.Common.IdentitySearchFactor searchFactor, string[] searchFactorValues, - MembershipQuery queryMembership, + TeamFoundation.Framework.Common.MembershipQuery queryMembership, ReadIdentityOptions readOptions) { return new[] { NullIdentities }; @@ -131,7 +131,7 @@ public TeamFoundation.Framework.Client.TeamFoundationIdentity[][] ReadIdentities public TeamFoundation.Framework.Client.TeamFoundationIdentity[] ReadIdentities( Guid[] teamFoundationIds, - MembershipQuery queryMembership, + TeamFoundation.Framework.Common.MembershipQuery queryMembership, ReadIdentityOptions readOptions, IEnumerable propertyNameFilters, IdentityPropertyScope propertyScope) @@ -142,7 +142,7 @@ public TeamFoundation.Framework.Client.TeamFoundationIdentity[] ReadIdentities( public TeamFoundation.Framework.Client.TeamFoundationIdentity[][] ReadIdentities( TeamFoundation.Framework.Common.IdentitySearchFactor searchFactor, string[] searchFactorValues, - MembershipQuery queryMembership, + TeamFoundation.Framework.Common.MembershipQuery queryMembership, ReadIdentityOptions readOptions, IEnumerable propertyNameFilters, IdentityPropertyScope propertyScope) @@ -152,7 +152,7 @@ public TeamFoundation.Framework.Client.TeamFoundationIdentity[][] ReadIdentities public TeamFoundation.Framework.Client.TeamFoundationIdentity[] ReadIdentities( TeamFoundation.Framework.Client.IdentityDescriptor[] descriptors, - MembershipQuery queryMembership, + TeamFoundation.Framework.Common.MembershipQuery queryMembership, ReadIdentityOptions readOptions, IEnumerable propertyNameFilters, IdentityPropertyScope propertyScope) @@ -163,7 +163,7 @@ public TeamFoundation.Framework.Client.TeamFoundationIdentity[] ReadIdentities( public TeamFoundation.Framework.Client.TeamFoundationIdentity ReadIdentity( TeamFoundation.Framework.Common.IdentitySearchFactor searchFactor, string searchFactorValue, - MembershipQuery queryMembership, + TeamFoundation.Framework.Common.MembershipQuery queryMembership, ReadIdentityOptions readOptions) { throw new NotImplementedException(); @@ -171,7 +171,7 @@ public TeamFoundation.Framework.Client.TeamFoundationIdentity ReadIdentity( public TeamFoundation.Framework.Client.TeamFoundationIdentity ReadIdentity( TeamFoundation.Framework.Client.IdentityDescriptor descriptor, - MembershipQuery queryMembership, + TeamFoundation.Framework.Common.MembershipQuery queryMembership, ReadIdentityOptions readOptions) { throw new NotSupportedException(); @@ -185,7 +185,7 @@ public TeamFoundation.Framework.Client.TeamFoundationIdentity ReadIdentity(strin public TeamFoundation.Framework.Client.TeamFoundationIdentity ReadIdentity( TeamFoundation.Framework.Common.IdentitySearchFactor searchFactor, string searchFactorValue, - MembershipQuery queryMembership, + TeamFoundation.Framework.Common.MembershipQuery queryMembership, ReadIdentityOptions readOptions, IEnumerable propertyNameFilters, IdentityPropertyScope propertyScope) @@ -195,7 +195,7 @@ public TeamFoundation.Framework.Client.TeamFoundationIdentity ReadIdentity( public TeamFoundation.Framework.Client.TeamFoundationIdentity ReadIdentity( TeamFoundation.Framework.Client.IdentityDescriptor descriptor, - MembershipQuery queryMembership, + TeamFoundation.Framework.Common.MembershipQuery queryMembership, ReadIdentityOptions readOptions, IEnumerable propertyNameFilters, IdentityPropertyScope propertyScope) @@ -233,52 +233,96 @@ public void UpdateExtendedProperties(TeamFoundation.Framework.Client.TeamFoundat throw new NotSupportedException(); } +#pragma warning disable RECS0154 // Parameter is never used public void AddRecentUser(TeamFoundationIdentity identity) +#pragma warning restore RECS0154 // Parameter is never used { throw new NotSupportedException(); } public TeamFoundation.Framework.Client.TeamFoundationIdentity[][] ReadIdentities( +#pragma warning disable RECS0154 // Parameter is never used IdentitySearchFactor searchFactor, +#pragma warning restore RECS0154 // Parameter is never used +#pragma warning disable RECS0154 // Parameter is never used string[] searchFactorValues, - MembershipQuery queryMembership, +#pragma warning restore RECS0154 // Parameter is never used +#pragma warning disable RECS0154 // Parameter is never used + TeamFoundation.Framework.Common.MembershipQuery queryMembership, +#pragma warning restore RECS0154 // Parameter is never used +#pragma warning disable RECS0154 // Parameter is never used ReadIdentityOptions readOptions) +#pragma warning restore RECS0154 // Parameter is never used { return new[] { NullIdentities }; } public TeamFoundation.Framework.Client.TeamFoundationIdentity[][] ReadIdentities( +#pragma warning disable RECS0154 // Parameter is never used IdentitySearchFactor searchFactor, +#pragma warning restore RECS0154 // Parameter is never used +#pragma warning disable RECS0154 // Parameter is never used string[] searchFactorValues, - MembershipQuery queryMembership, +#pragma warning restore RECS0154 // Parameter is never used +#pragma warning disable RECS0154 // Parameter is never used + TeamFoundation.Framework.Common.MembershipQuery queryMembership, +#pragma warning restore RECS0154 // Parameter is never used +#pragma warning disable RECS0154 // Parameter is never used ReadIdentityOptions readOptions, +#pragma warning restore RECS0154 // Parameter is never used +#pragma warning disable RECS0154 // Parameter is never used IEnumerable propertyNameFilters, +#pragma warning restore RECS0154 // Parameter is never used +#pragma warning disable RECS0154 // Parameter is never used IdentityPropertyScope propertyScope) +#pragma warning restore RECS0154 // Parameter is never used { throw new NotSupportedException(); } public TeamFoundation.Framework.Client.TeamFoundationIdentity ReadIdentity( +#pragma warning disable RECS0154 // Parameter is never used IdentitySearchFactor searchFactor, +#pragma warning restore RECS0154 // Parameter is never used +#pragma warning disable RECS0154 // Parameter is never used string searchFactorValue, - MembershipQuery queryMembership, +#pragma warning restore RECS0154 // Parameter is never used +#pragma warning disable RECS0154 // Parameter is never used + TeamFoundation.Framework.Common.MembershipQuery queryMembership, +#pragma warning restore RECS0154 // Parameter is never used +#pragma warning disable RECS0154 // Parameter is never used ReadIdentityOptions readOptions) +#pragma warning restore RECS0154 // Parameter is never used { throw new NotSupportedException(); } public TeamFoundation.Framework.Client.TeamFoundationIdentity ReadIdentity( +#pragma warning disable RECS0154 // Parameter is never used IdentitySearchFactor searchFactor, +#pragma warning restore RECS0154 // Parameter is never used +#pragma warning disable RECS0154 // Parameter is never used string searchFactorValue, - MembershipQuery queryMembership, +#pragma warning restore RECS0154 // Parameter is never used +#pragma warning disable RECS0154 // Parameter is never used + TeamFoundation.Framework.Common.MembershipQuery queryMembership, +#pragma warning restore RECS0154 // Parameter is never used +#pragma warning disable RECS0154 // Parameter is never used ReadIdentityOptions readOptions, +#pragma warning restore RECS0154 // Parameter is never used +#pragma warning disable RECS0154 // Parameter is never used IEnumerable propertyNameFilters, +#pragma warning restore RECS0154 // Parameter is never used +#pragma warning disable RECS0154 // Parameter is never used IdentityPropertyScope propertyScope) +#pragma warning restore RECS0154 // Parameter is never used { throw new NotSupportedException(); } +#pragma warning disable RECS0154 // Parameter is never used public void UpdateExtendedProperties(TeamFoundationIdentity identity) +#pragma warning restore RECS0154 // Parameter is never used { throw new NotSupportedException(); } diff --git a/test/Qwiq.Identity.Tests/Qwiq.Identity.UnitTests.csproj b/test/Qwiq.Identity.Tests/Qwiq.Identity.UnitTests.csproj index 466f2155..d43651b4 100644 --- a/test/Qwiq.Identity.Tests/Qwiq.Identity.UnitTests.csproj +++ b/test/Qwiq.Identity.Tests/Qwiq.Identity.UnitTests.csproj @@ -1,7 +1,7 @@  - - + + {CE68530E-EB8F-4BE2-9563-A09AC70EA8C1} @@ -151,10 +151,10 @@ ..\..\packages\Microsoft.VisualStudio.Services.Client.15.112.1\lib\net45\Microsoft.VisualStudio.Services.WebApi.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll ..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll @@ -234,13 +234,13 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - + + + - + \ No newline at end of file diff --git a/test/Qwiq.Identity.Tests/packages.config b/test/Qwiq.Identity.Tests/packages.config index fc27df2c..8a353099 100644 --- a/test/Qwiq.Identity.Tests/packages.config +++ b/test/Qwiq.Identity.Tests/packages.config @@ -4,15 +4,15 @@ - + - - + + diff --git a/test/Qwiq.Integration.Tests/Identity/Soap/IdentityManagementServiceTests.cs b/test/Qwiq.Integration.Tests/Identity/Soap/IdentityManagementServiceTests.cs new file mode 100644 index 00000000..a6013762 --- /dev/null +++ b/test/Qwiq.Integration.Tests/Identity/Soap/IdentityManagementServiceTests.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Should; + +namespace Microsoft.Qwiq.Identity.Soap +{ + [TestClass] + public class Given_an_Account_with_Group_Membership : SoapIdentityManagementServiceContextSpecification + { + private string _input; + private ITeamFoundationIdentity _result; + + public override void Given() + { + base.Given(); + + _input = "rimuri@microsoft.com"; + } + + public override void When() + { + _result = Instance.ReadIdentity(IdentitySearchFactor.AccountName, _input, MembershipQuery.Expanded); + } + + [TestMethod] + [TestCategory("localOnly")] + [TestCategory("SOAP")] + public void Identity_Contains_MemberOf() + { + _result.MemberOf.Any().ShouldBeTrue(); + } + } +} diff --git a/test/Qwiq.Integration.Tests/Identity/Soap/SoapIdentityMapperContextSpecification.cs b/test/Qwiq.Integration.Tests/Identity/Soap/SoapIdentityMapperContextSpecification.cs index 6c577495..de29dfb1 100644 --- a/test/Qwiq.Integration.Tests/Identity/Soap/SoapIdentityMapperContextSpecification.cs +++ b/test/Qwiq.Integration.Tests/Identity/Soap/SoapIdentityMapperContextSpecification.cs @@ -1,4 +1,5 @@ -using Microsoft.Qwiq.Tests.Common; +using System.Collections.Generic; +using Microsoft.Qwiq.Tests.Common; using Microsoft.VisualStudio.TestTools.UnitTesting; using Should; @@ -27,9 +28,23 @@ public override void Given() public override void When() { -#pragma warning disable 0618 - ActualOutput = TimedAction(() => (T)Instance.Map(Input), "SOAP", "Map"); -#pragma warning restore 0618 +#pragma warning disable IDE0019 // Use pattern matching + var stringValue = Input as string; + var stringArray = Input as IEnumerable; +#pragma warning restore IDE0019 // Use pattern matching + + if (stringValue != null) + { + ActualOutput = TimedAction(() => (T)Instance.Map(stringValue), "SOAP", "Map"); + } + else if (stringArray != null) + { + ActualOutput = TimedAction(() => (T)Instance.Map(stringArray), "SOAP", "Map"); + } + else + { + ActualOutput = Input; + } } [TestMethod] diff --git a/test/Qwiq.Integration.Tests/IntegrationSettings.cs b/test/Qwiq.Integration.Tests/IntegrationSettings.cs index ee5b2efc..e9eaadc9 100644 --- a/test/Qwiq.Integration.Tests/IntegrationSettings.cs +++ b/test/Qwiq.Integration.Tests/IntegrationSettings.cs @@ -24,8 +24,7 @@ public static class IntegrationSettings private static readonly Uri Uri = new Uri("https://microsoft.visualstudio.com/defaultcollection"); /// - public static AuthenticationOptions AuthenticationOptions { get; } = - new AuthenticationOptions(Uri, AuthenticationTypes.Windows); + public static AuthenticationOptions AuthenticationOptions { get; } = new AuthenticationOptions(Uri, AuthenticationTypes.Windows, Credentials); /// public static Func CreateRestStore { get; } = () => diff --git a/test/Qwiq.Integration.Tests/Mapper/AttributeMapperTests.cs b/test/Qwiq.Integration.Tests/Mapper/AttributeMapperTests.cs index cfe46865..1eb5f44c 100644 --- a/test/Qwiq.Integration.Tests/Mapper/AttributeMapperTests.cs +++ b/test/Qwiq.Integration.Tests/Mapper/AttributeMapperTests.cs @@ -22,8 +22,6 @@ protected override void ConfigureOptions() { WorkItemStore.Configuration.DefaultFields = new[] { - //CoreFieldRefNames.TeamProject, - //CoreFieldRefNames.WorkItemType, CoreFieldRefNames.Id, CoreFieldRefNames.State }; @@ -38,7 +36,6 @@ public class [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); @@ -69,7 +66,6 @@ protected override void ConfigureOptions() WorkItemStore.Configuration.DefaultFields = new[] { CoreFieldRefNames.TeamProject, - //CoreFieldRefNames.WorkItemType, CoreFieldRefNames.Id, CoreFieldRefNames.State }; @@ -83,7 +79,6 @@ public class [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); diff --git a/test/Qwiq.Integration.Tests/Mapper/Identity/DisplayNameToAliasConverterContextSpecification.cs b/test/Qwiq.Integration.Tests/Mapper/Identity/DisplayNameToAliasConverterContextSpecification.cs index d38f41e4..4065e5bf 100644 --- a/test/Qwiq.Integration.Tests/Mapper/Identity/DisplayNameToAliasConverterContextSpecification.cs +++ b/test/Qwiq.Integration.Tests/Mapper/Identity/DisplayNameToAliasConverterContextSpecification.cs @@ -15,12 +15,6 @@ public abstract class DisplayNameToAliasConverterContextSpecification : SoapIden protected DisplayNameToAliasValueConverter ValueConverter { get; private set; } - [TestMethod] - public void Converted_value_is_expected_type() - { - ConvertedValue.ShouldBeType(typeof(Dictionary)); - } - /// public override void Given() { diff --git a/test/Qwiq.Integration.Tests/Mapper/Identity/DisplayNameToAliasConverterTests.cs b/test/Qwiq.Integration.Tests/Mapper/Identity/DisplayNameToAliasConverterTests.cs index ab92e2b7..c87c46e7 100644 --- a/test/Qwiq.Integration.Tests/Mapper/Identity/DisplayNameToAliasConverterTests.cs +++ b/test/Qwiq.Integration.Tests/Mapper/Identity/DisplayNameToAliasConverterTests.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; - +using Microsoft.Qwiq.Identity; using Microsoft.VisualStudio.TestTools.UnitTesting; using Should; @@ -13,7 +13,7 @@ public class Given_multiple_display_names : MultipleDisplayNameContextSpecificat public override void Given() { base.Given(); - DisplayNames = new[]{"Peter Lavallee", "Jason Weber"}; + DisplayNames = new[] { "Peter Lavallee", "Jason Weber" }; } [TestMethod] @@ -21,9 +21,30 @@ public override void Given() [TestCategory("SOAP")] public void Converted_value_result_is_expected_value() { - var kvp = ((Dictionary)ConvertedValue); - kvp.First().Value.ShouldEqual("pelavall"); - kvp.Skip(1).Single().Value.ShouldEqual("jweber"); + ConvertedValue.ShouldBeNull(); + } + + [TestMethod] + [TestCategory("localOnly")] + [TestCategory("SOAP")] + [Ignore] + public void Converted_value_contains_a_single_result() + { + Assert.Inconclusive(); + } + + [TestMethod] + [TestCategory("localOnly")] + [TestCategory("SOAP")] + [Ignore] + public new void Converted_value_contains_a_expected_number_of_results() + { + Assert.Inconclusive(); + } + + public override void When() + { + Assert.ThrowsException(() => ValueConverter.Map(DisplayNames)); } } @@ -35,6 +56,28 @@ public override void Given() base.Given(); DisplayNames = new[] { "Peter Lavallee ", "Jason Weber " }; } + + [TestMethod] + [TestCategory("localOnly")] + [TestCategory("SOAP")] + public new void Converted_value_contains_a_expected_number_of_results() + { + var kvp = (Dictionary)ConvertedValue; + kvp.Count.ShouldEqual(DisplayNames.Length); + } + + [TestMethod] + [TestCategory("localOnly")] + [TestCategory("SOAP")] + public new void Converted_value_result_is_expected_value() + { + ConvertedValue.ShouldBeType>(); + } + + public override void When() + { + ConvertedValue = TimedAction(() => ValueConverter.Map(DisplayNames), "SOAP", "Map"); + } } [TestClass] @@ -51,8 +94,8 @@ public override void Given() [TestCategory("SOAP")] public void Converted_value_result_is_expected_value() { - var kvp = ((Dictionary)ConvertedValue); - kvp.First().Value.ShouldEqual("pelavall"); + var kvp = (string)ConvertedValue; + kvp.ShouldEqual("pelavall"); } } @@ -82,8 +125,21 @@ public override void Given() [TestCategory("SOAP")] public void Converted_value_result_is_expected_value() { - var kvp = ((Dictionary)ConvertedValue); - kvp.First().Value.ShouldEqual("jweber"); + ConvertedValue.ShouldBeNull(); + } + + [TestMethod] + [TestCategory("localOnly")] + [TestCategory("SOAP")] + [Ignore] + public new void Converted_value_contains_a_single_result() + { + Assert.Inconclusive(); + } + + public override void When() + { + Assert.ThrowsException(() => ValueConverter.Map(DisplayName)); } } @@ -96,5 +152,18 @@ public override void Given() base.Given(); DisplayName = "Jason Weber "; } + + public override void When() + { + ConvertedValue = TimedAction(() => ValueConverter.Map(DisplayName), "SOAP", "Map"); + } + + [TestMethod] + [TestCategory("localOnly")] + [TestCategory("SOAP")] + public new void Converted_value_result_is_expected_value() + { + ((string)ConvertedValue).ShouldEqual("jweber", Comparer.OrdinalIgnoreCase); + } } } diff --git a/test/Qwiq.Integration.Tests/Mapper/Identity/MultipleDisplayNameContextSpecification.cs b/test/Qwiq.Integration.Tests/Mapper/Identity/MultipleDisplayNameContextSpecification.cs index e0a2af6f..e37b8ed8 100644 --- a/test/Qwiq.Integration.Tests/Mapper/Identity/MultipleDisplayNameContextSpecification.cs +++ b/test/Qwiq.Integration.Tests/Mapper/Identity/MultipleDisplayNameContextSpecification.cs @@ -13,7 +13,7 @@ public class MultipleDisplayNameContextSpecification : DisplayNameToAliasConvert [TestCategory("SOAP")] public void Converted_value_contains_a_expected_number_of_results() { - var kvp = (Dictionary)ConvertedValue; + var kvp = (Dictionary)ConvertedValue; kvp.Count.ShouldEqual(DisplayNames.Length); } diff --git a/test/Qwiq.Integration.Tests/Mapper/Identity/SingleDisplayNameContextSpecification.cs b/test/Qwiq.Integration.Tests/Mapper/Identity/SingleDisplayNameContextSpecification.cs index 0e89eb30..e225ce1a 100644 --- a/test/Qwiq.Integration.Tests/Mapper/Identity/SingleDisplayNameContextSpecification.cs +++ b/test/Qwiq.Integration.Tests/Mapper/Identity/SingleDisplayNameContextSpecification.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Should; @@ -16,8 +14,8 @@ public class SingleDisplayNameContextSpecification : DisplayNameToAliasConverter [TestCategory("SOAP")] public void Converted_value_contains_a_single_result() { - var kvp = (Dictionary)ConvertedValue; - kvp.Count.ShouldEqual(1); + var val = (string)ConvertedValue; + val.ShouldNotBeNull(); } /// diff --git a/test/Qwiq.Integration.Tests/Mapper/WiqlAttributeMapperContextSpecification.cs b/test/Qwiq.Integration.Tests/Mapper/WiqlAttributeMapperContextSpecification.cs index cd6f54cc..039e842e 100644 --- a/test/Qwiq.Integration.Tests/Mapper/WiqlAttributeMapperContextSpecification.cs +++ b/test/Qwiq.Integration.Tests/Mapper/WiqlAttributeMapperContextSpecification.cs @@ -29,7 +29,7 @@ public override void Given() var pr = new PropertyReflector(); var pi = new PropertyInspector(pr); var attMapper = new AttributeMapperStrategy(pi); - var mapper = new WorkItemMapper(new IWorkItemMapperStrategy[] { attMapper }); + var mapper = new WorkItemMapper(attMapper); var translator = new WiqlTranslator(); var pe = new PartialEvaluator(); var qr = new QueryRewriter(); diff --git a/test/Qwiq.Integration.Tests/Project/ProjectTests.cs b/test/Qwiq.Integration.Tests/Project/ProjectTests.cs index c76e57ae..5c740d82 100644 --- a/test/Qwiq.Integration.Tests/Project/ProjectTests.cs +++ b/test/Qwiq.Integration.Tests/Project/ProjectTests.cs @@ -21,14 +21,40 @@ public void Each_project_equals_eachother() RestProject.GetHashCode().ShouldEqual(SoapProject.GetHashCode()); } + [TestMethod] + [TestCategory("localOnly")] + [TestCategory("SOAP")] + [TestCategory("REST")] + public void Each_project_contains_the_same_materialized_Area_paths() + { + var r = TimedAction(() => RestProject.AreaRootNodes.ToList(), "REST", "AreaRootNodes.ToList()"); + var s = TimedAction(() => SoapProject.AreaRootNodes.ToList(), "SOAP", "AreaRootNodes.ToList()"); + + r.ShouldContainOnly(s, WorkItemClassificationNodeComparer.Default); + // NOTE: Hashcodes will be different as they use the List's functionality + } + + [TestMethod] + [TestCategory("localOnly")] + [TestCategory("SOAP")] + [TestCategory("REST")] + public void Each_project_contains_the_same_materialized_Iteration_paths() + { + var r = TimedAction(() => RestProject.IterationRootNodes.ToList(), "REST", "IterationRootNodes.ToList()"); + var s = TimedAction(() => SoapProject.IterationRootNodes.ToList(), "SOAP", "IterationRootNodes.ToList()"); + + r.ShouldContainOnly(s, WorkItemClassificationNodeComparer.Default); + // NOTE: Hashcodes will be different as they use the List's functionality + } + [TestMethod] [TestCategory("localOnly")] [TestCategory("SOAP")] [TestCategory("REST")] public void Each_project_contains_the_same_Area_paths() { - RestProject.AreaRootNodes.ShouldContainOnly(SoapProject.AreaRootNodes, NodeComparer.Default); - RestProject.AreaRootNodes.ShouldEqual(SoapProject.AreaRootNodes, Comparer.NodeCollection); + RestProject.AreaRootNodes.ShouldContainOnly(SoapProject.AreaRootNodes, WorkItemClassificationNodeComparer.Default); + RestProject.AreaRootNodes.ShouldEqual(SoapProject.AreaRootNodes, Comparer.WorkItemClassificationNodeCollection); RestProject.AreaRootNodes.GetHashCode().ShouldEqual(SoapProject.AreaRootNodes.GetHashCode()); } @@ -38,8 +64,8 @@ public void Each_project_contains_the_same_Area_paths() [TestCategory("REST")] public void Each_project_contains_the_same_Iteration_paths() { - RestProject.IterationRootNodes.ShouldContainOnly(SoapProject.IterationRootNodes, NodeComparer.Default); - RestProject.IterationRootNodes.ShouldEqual(SoapProject.IterationRootNodes, Comparer.NodeCollection); + RestProject.IterationRootNodes.ShouldContainOnly(SoapProject.IterationRootNodes, WorkItemClassificationNodeComparer.Default); + RestProject.IterationRootNodes.ShouldEqual(SoapProject.IterationRootNodes, Comparer.WorkItemClassificationNodeCollection); RestProject.IterationRootNodes.GetHashCode().ShouldEqual(SoapProject.IterationRootNodes.GetHashCode()); } diff --git a/test/Qwiq.Integration.Tests/Qwiq.IntegrationTests.csproj b/test/Qwiq.Integration.Tests/Qwiq.IntegrationTests.csproj index eb718ed2..af8045a0 100644 --- a/test/Qwiq.Integration.Tests/Qwiq.IntegrationTests.csproj +++ b/test/Qwiq.Integration.Tests/Qwiq.IntegrationTests.csproj @@ -1,7 +1,7 @@  - - + + {E4130432-C890-41E0-8407-C4142CAF59D8} @@ -154,10 +154,10 @@ ..\..\packages\Microsoft.VisualStudio.Services.Client.15.112.1\lib\net45\Microsoft.VisualStudio.Services.WebApi.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll ..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll @@ -191,6 +191,7 @@ + @@ -213,7 +214,6 @@ - @@ -270,6 +270,10 @@ {016E8D93-4195-4639-BCD5-77633E8E1681} Qwiq.Mapper + + {ce68530e-eb8f-4be2-9563-a09ac70ea8c1} + Qwiq.Identity.UnitTests + {B45C92B0-AC36-409D-86A5-5428C87384C3} Qwiq.Tests.Common @@ -281,13 +285,13 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - + + + - + \ No newline at end of file diff --git a/test/Qwiq.Integration.Tests/WorkItemStore/Soap/WorkItemStoreFactoryTests.cs b/test/Qwiq.Integration.Tests/WorkItemStore/Soap/WorkItemStoreFactoryTests.cs deleted file mode 100644 index 42239201..00000000 --- a/test/Qwiq.Integration.Tests/WorkItemStore/Soap/WorkItemStoreFactoryTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; - -using Microsoft.Qwiq.Credentials; -using Microsoft.VisualStudio.Services.Client; -using Microsoft.VisualStudio.Services.Common; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using Should; - -namespace Microsoft.Qwiq.WorkItemStore.Soap -{ - [TestClass] - public class Given_a_Uri_and_Credential : WorkItemStoreFactoryContextSpecification - { - public override IWorkItemStore Create() - { - var uri = new Uri("https://microsoft.visualstudio.com/DefaultCollection"); - var cred = new VssClientCredentials( - new WindowsCredential(true), - CredentialPromptType.PromptIfNeeded) - { - Storage = new VssClientCredentialStorage() - }; - -#pragma warning disable CS0618 // Type or member is obsolete - return Instance.Create( - uri, - new TfsCredentials(cred)); -#pragma warning restore CS0618 // Type or member is obsolete - } - - [TestMethod] - [TestCategory("localOnly")] - [TestCategory("SOAP")] - public void Store_is_Created() - { - WorkItemStore.ShouldNotBeNull(); - } - } - - [TestClass] - public class Given_a_Uri_and_Credentials_from_the_CredentialsFactory : WorkItemStoreFactoryContextSpecification - { - public override IWorkItemStore Create() - { - var uri = new Uri("https://microsoft.visualstudio.com/DefaultCollection"); - - -#pragma warning disable CS0618 // Type or member is obsolete - return Instance.Create( - uri, - CredentialsFactory.CreateCredentials((string)null)); -#pragma warning restore CS0618 // Type or member is obsolete - } - - - - [TestMethod] - [TestCategory("localOnly")] - [TestCategory("SOAP")] - public void Store_is_Created() - { - WorkItemStore.ShouldNotBeNull(); - } - } -} diff --git a/test/Qwiq.Integration.Tests/packages.config b/test/Qwiq.Integration.Tests/packages.config index 98d952e8..b4a6de85 100644 --- a/test/Qwiq.Integration.Tests/packages.config +++ b/test/Qwiq.Integration.Tests/packages.config @@ -5,15 +5,15 @@ - + - - + + diff --git a/test/Qwiq.Linq.Tests/QueryBuilderTests.cs b/test/Qwiq.Linq.Tests/QueryBuilderTests.cs index be6b651a..a7e368c6 100644 --- a/test/Qwiq.Linq.Tests/QueryBuilderTests.cs +++ b/test/Qwiq.Linq.Tests/QueryBuilderTests.cs @@ -80,10 +80,10 @@ public override void When() { base.When(); Expected = - "SELECT * FROM WorkItems WHERE (([Title] = 'asdf') AND ([Keywords] = 'String Value') AND ([Created Date] > '2012-11-29 17:00:00Z'))"; + "SELECT * FROM WorkItems WHERE (([Title] = 'asdf') AND ([Tags] = 'String Value') AND ([Created Date] > '2012-11-29 17:00:00Z'))"; Actual = Query.Where(item => item.Title == "asdf") - .Where(item => item.Keywords == "String Value") + .Where(item => item.Tags == "String Value") .Where(item => item.CreatedDate > _date) .ToString(); } @@ -102,8 +102,8 @@ public class when_a_query_has_a_where_clause_with_an_or_expression : WiqlQueryBu public override void When() { base.When(); - Expected = "SELECT * FROM WorkItems WHERE ((([Keywords] = 'person1') OR ([Keywords] = 'person2')))"; - Actual = Query.Where(item => item.Keywords == "person1" || item.Keywords == "person2").ToString(); + Expected = "SELECT * FROM WorkItems WHERE ((([Tags] = 'person1') OR ([Tags] = 'person2')))"; + Actual = Query.Where(item => item.Tags == "person1" || item.Tags == "person2").ToString(); } [TestMethod] @@ -128,8 +128,8 @@ public override void Given() public override void When() { base.When(); - Expected = "SELECT * FROM WorkItems WHERE (([Keywords] IN ('person1', 'person2')))"; - Actual = Query.Where(item => _values.Contains(item.Keywords)).ToString(); + Expected = "SELECT * FROM WorkItems WHERE (([Tags] IN ('person1', 'person2')))"; + Actual = Query.Where(item => _values.Contains(item.Tags)).ToString(); } [TestMethod] @@ -154,8 +154,8 @@ public override void Given() public override void When() { base.When(); - Expected = "SELECT * FROM WorkItems WHERE (([Keywords] IN ('person1', 'person2')))"; - Actual = Query.Where(item => _values.Contains(item.Keywords)).ToString(); + Expected = "SELECT * FROM WorkItems WHERE (([Tags] IN ('person1', 'person2')))"; + Actual = Query.Where(item => _values.Contains(item.Tags)).ToString(); } [TestMethod] @@ -181,7 +181,7 @@ public override void Given() [ExpectedException(typeof(NotSupportedException))] public void it_is_not_supported() { - Actual = Query.Where(item => _values.Contains(item.Keywords)).ToString(); + Actual = Query.Where(item => _values.Contains(item.Tags)).ToString(); } } @@ -205,7 +205,7 @@ public override void Given() [ExpectedException(typeof(NotSupportedException))] public void it_is_not_supported() { - Actual = Query.Where(item => _values.Contains(item.Keywords)).ToString(); + Actual = Query.Where(item => _values.Contains(item.Tags)).ToString(); } } @@ -399,6 +399,42 @@ public void the_WasEver_is_translated_to_an_ever_operator() } } + [TestClass] + // ReSharper disable once InconsistentNaming + public class when_a_where_clause_uses_the_ingroup_function : WiqlQueryBuilderContextSpecification + { + public override void When() + { + base.When(); + Expected = "SELECT * FROM WorkItems WHERE (([Assigned To] IN GROUP 'o_alias'))"; + Actual = Query.Where(item => item.AssignedTo.InGroup("o_alias")).ToString(); + } + + [TestMethod] + public void the_InGroup_is_translated_to_an_InGroup_operator() + { + Actual.ShouldEqual(Expected); + } + } + + [TestClass] + // ReSharper disable once InconsistentNaming + public class when_a_where_clause_uses_the_notingroup_function : WiqlQueryBuilderContextSpecification + { + public override void When() + { + base.When(); + Expected = "SELECT * FROM WorkItems WHERE (([Assigned To] NOT IN GROUP 'o_alias'))"; + Actual = Query.Where(item => item.AssignedTo.NotInGroup("o_alias")).ToString(); + } + + [TestMethod] + public void the_NotInGroup_is_translated_to_an_InGroup_operator() + { + Actual.ShouldEqual(Expected); + } + } + [TestClass] // ReSharper disable once InconsistentNaming public class when_a_where_clause_uses_the_Contains_string_function : WiqlQueryBuilderContextSpecification diff --git a/test/Qwiq.Linq.Tests/Qwiq.Linq.UnitTests.csproj b/test/Qwiq.Linq.Tests/Qwiq.Linq.UnitTests.csproj index 01b73458..5883e0b3 100644 --- a/test/Qwiq.Linq.Tests/Qwiq.Linq.UnitTests.csproj +++ b/test/Qwiq.Linq.Tests/Qwiq.Linq.UnitTests.csproj @@ -1,7 +1,7 @@  - - + + {0A26136B-A086-4CDF-BFF6-E0A83FB67446} @@ -23,10 +23,10 @@ - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll ..\..\packages\Should.1.1.20\lib\Should.dll @@ -69,11 +69,11 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - + + + - + \ No newline at end of file diff --git a/test/Qwiq.Linq.Tests/packages.config b/test/Qwiq.Linq.Tests/packages.config index c6113f70..b9d35a84 100644 --- a/test/Qwiq.Linq.Tests/packages.config +++ b/test/Qwiq.Linq.Tests/packages.config @@ -1,8 +1,8 @@  - - - + + + \ No newline at end of file diff --git a/test/Qwiq.Mapper.Benchmark.Tests/PocoMapping.cs b/test/Qwiq.Mapper.Benchmark.Tests/PocoMapping.cs index c7d89cd8..c87d8b99 100644 --- a/test/Qwiq.Mapper.Benchmark.Tests/PocoMapping.cs +++ b/test/Qwiq.Mapper.Benchmark.Tests/PocoMapping.cs @@ -23,6 +23,9 @@ public override void When() [TestMethod] +#if DEBUG + [Ignore] +#endif [TestCategory(Constants.TestCategory.Benchmark)] [TestCategory(Constants.TestCategory.Performance)] public void Execute_Mapping_Performance_Benchmark() @@ -40,7 +43,7 @@ public class Benchmark private Type _type; - [Setup] + [GlobalSetup] public void SetupData() { var propertyInspector = new PropertyInspector(new PropertyReflector()); diff --git a/test/Qwiq.Mapper.Benchmark.Tests/PocoMappingWithLinks.cs b/test/Qwiq.Mapper.Benchmark.Tests/PocoMappingWithLinks.cs index 4fa59e02..f351b1e3 100644 --- a/test/Qwiq.Mapper.Benchmark.Tests/PocoMappingWithLinks.cs +++ b/test/Qwiq.Mapper.Benchmark.Tests/PocoMappingWithLinks.cs @@ -25,6 +25,9 @@ public override void When() } [TestMethod] +#if DEBUG + [Ignore] +#endif [TestCategory(Constants.TestCategory.Benchmark)] [TestCategory(Constants.TestCategory.Performance)] public void Execute_Mapping_with_Links_Performance_Benchmark() @@ -38,7 +41,7 @@ public class Benchmark private WorkItemMapper _mapper; private IEnumerable _items; - [Setup] + [GlobalSetup] public void SetupData() { var wis = new MockWorkItemStore(); diff --git a/test/Qwiq.Mapper.Benchmark.Tests/Qwiq.Mapper.BenchmarkTests.csproj b/test/Qwiq.Mapper.Benchmark.Tests/Qwiq.Mapper.BenchmarkTests.csproj index 5212213e..2f9e7738 100644 --- a/test/Qwiq.Mapper.Benchmark.Tests/Qwiq.Mapper.BenchmarkTests.csproj +++ b/test/Qwiq.Mapper.Benchmark.Tests/Qwiq.Mapper.BenchmarkTests.csproj @@ -1,7 +1,7 @@  - - + + @@ -23,35 +23,39 @@ - - ..\..\packages\BenchmarkDotNet.0.10.5\lib\net46\BenchmarkDotNet.dll + + ..\..\packages\BenchmarkDotNet.0.10.11\lib\net46\BenchmarkDotNet.dll - - ..\..\packages\BenchmarkDotNet.Core.0.10.5\lib\net46\BenchmarkDotNet.Core.dll + + ..\..\packages\BenchmarkDotNet.Core.0.10.11\lib\net46\BenchmarkDotNet.Core.dll - - ..\..\packages\BenchmarkDotNet.Toolchains.Roslyn.0.10.5\lib\net46\BenchmarkDotNet.Toolchains.Roslyn.dll + + ..\..\packages\BenchmarkDotNet.Toolchains.Roslyn.0.10.11\lib\net46\BenchmarkDotNet.Toolchains.Roslyn.dll - - ..\..\packages\Microsoft.CodeAnalysis.Common.2.1.0\lib\netstandard1.3\Microsoft.CodeAnalysis.dll + + ..\..\packages\Microsoft.CodeAnalysis.Common.2.6.1\lib\netstandard1.3\Microsoft.CodeAnalysis.dll - - ..\..\packages\Microsoft.CodeAnalysis.CSharp.2.1.0\lib\netstandard1.3\Microsoft.CodeAnalysis.CSharp.dll + + ..\..\packages\Microsoft.CodeAnalysis.CSharp.2.6.1\lib\netstandard1.3\Microsoft.CodeAnalysis.CSharp.dll + ..\..\packages\Microsoft.DotNet.InternalAbstractions.1.0.0\lib\net451\Microsoft.DotNet.InternalAbstractions.dll - - ..\..\packages\Microsoft.DotNet.PlatformAbstractions.1.1.1\lib\net451\Microsoft.DotNet.PlatformAbstractions.dll + + ..\..\packages\Microsoft.DotNet.PlatformAbstractions.2.0.4\lib\net45\Microsoft.DotNet.PlatformAbstractions.dll ..\..\packages\WindowsAzure.ServiceBus.3.3.2\lib\net45-full\Microsoft.ServiceBus.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + ..\..\packages\Microsoft.Win32.Registry.4.4.0\lib\net46\Microsoft.Win32.Registry.dll ..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll @@ -63,9 +67,8 @@ ..\..\packages\System.AppContext.4.3.0\lib\net46\System.AppContext.dll - - ..\..\packages\System.Collections.Immutable.1.3.1\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll - True + + ..\..\packages\System.Collections.Immutable.1.4.0\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll @@ -90,12 +93,13 @@ - - ..\..\packages\System.Reflection.Metadata.1.4.2\lib\portable-net45+win8\System.Reflection.Metadata.dll + + ..\..\packages\System.Reflection.Metadata.1.5.0\lib\portable-net45+win8\System.Reflection.Metadata.dll - ..\..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net46\System.Security.Cryptography.Algorithms.dll + ..\..\packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net46\System.Security.Cryptography.Algorithms.dll + True ..\..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll @@ -104,25 +108,26 @@ ..\..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll - ..\..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net46\System.Security.Cryptography.X509Certificates.dll + ..\..\packages\System.Security.Cryptography.X509Certificates.4.3.2\lib\net46\System.Security.Cryptography.X509Certificates.dll + True - ..\..\packages\System.Text.Encoding.CodePages.4.3.0\lib\net46\System.Text.Encoding.CodePages.dll + ..\..\packages\System.Text.Encoding.CodePages.4.4.0\lib\net46\System.Text.Encoding.CodePages.dll - - ..\..\packages\System.Threading.Tasks.Extensions.4.3.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll + + ..\..\packages\System.Threading.Tasks.Extensions.4.4.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll ..\..\packages\System.Threading.Thread.4.3.0\lib\net46\System.Threading.Thread.dll - - ..\..\packages\System.ValueTuple.4.3.0\lib\netstandard1.0\System.ValueTuple.dll + + ..\..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll - ..\..\packages\System.Xml.ReaderWriter.4.3.0\lib\net46\System.Xml.ReaderWriter.dll + ..\..\packages\System.Xml.ReaderWriter.4.3.1\lib\net46\System.Xml.ReaderWriter.dll ..\..\packages\System.Xml.XmlDocument.4.3.0\lib\net46\System.Xml.XmlDocument.dll @@ -184,11 +189,11 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - + + + - + \ No newline at end of file diff --git a/test/Qwiq.Mapper.Benchmark.Tests/app.config b/test/Qwiq.Mapper.Benchmark.Tests/app.config index 720da180..d1a6b0f4 100644 --- a/test/Qwiq.Mapper.Benchmark.Tests/app.config +++ b/test/Qwiq.Mapper.Benchmark.Tests/app.config @@ -12,11 +12,11 @@ - + - + @@ -24,7 +24,7 @@ - + @@ -36,11 +36,11 @@ - + - + @@ -74,6 +74,18 @@ + + + + + + + + + + + + diff --git a/test/Qwiq.Mapper.Benchmark.Tests/packages.config b/test/Qwiq.Mapper.Benchmark.Tests/packages.config index ec447c62..bd4f14eb 100644 --- a/test/Qwiq.Mapper.Benchmark.Tests/packages.config +++ b/test/Qwiq.Mapper.Benchmark.Tests/packages.config @@ -1,23 +1,24 @@  - - - + + + - - + + - - - - + + + + + - + @@ -31,30 +32,31 @@ - - + + - + - + - + - + - + - - + + + diff --git a/test/Qwiq.Mapper.Tests/Qwiq.Mapper.UnitTests.csproj b/test/Qwiq.Mapper.Tests/Qwiq.Mapper.UnitTests.csproj index 0c90d01d..cbb3d9ae 100644 --- a/test/Qwiq.Mapper.Tests/Qwiq.Mapper.UnitTests.csproj +++ b/test/Qwiq.Mapper.Tests/Qwiq.Mapper.UnitTests.csproj @@ -1,7 +1,7 @@  - - + + {BECF2066-A6C5-475A-BA14-12890F284A12} @@ -32,10 +32,10 @@ ..\..\packages\Microsoft.VisualStudio.Services.Client.15.112.1\lib\net45\Microsoft.VisualStudio.Services.WebApi.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll ..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll @@ -104,11 +104,11 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - + + + - + \ No newline at end of file diff --git a/test/Qwiq.Mapper.Tests/WorkItemMapperTests.cs b/test/Qwiq.Mapper.Tests/WorkItemMapperTests.cs index 30bf03de..5cb45002 100644 --- a/test/Qwiq.Mapper.Tests/WorkItemMapperTests.cs +++ b/test/Qwiq.Mapper.Tests/WorkItemMapperTests.cs @@ -57,9 +57,20 @@ public abstract class WorkItemMapperContext : ContextSpecification } }; + protected WorkItemMapper CreateNoExceptionMapper() + { + var propertyInspector = new PropertyInspector(new PropertyReflector()); + var mappingStrategies = new IWorkItemMapperStrategy[] + { + new NoExceptionAttributeMapperStrategy(propertyInspector), + new WorkItemLinksMapperStrategy(propertyInspector, WorkItemStore) + }; + return new WorkItemMapper(mappingStrategies); + } + protected IWorkItemStore WorkItemStore; - private IWorkItemMapper _workItemMapper; + protected IWorkItemMapper WorkItemMapper; protected IEnumerable SourceWorkItems; @@ -73,12 +84,12 @@ public override void Given() new AttributeMapperStrategy(propertyInspector), new WorkItemLinksMapperStrategy(propertyInspector, WorkItemStore) }; - _workItemMapper = new WorkItemMapper(mappingStrategies); + WorkItemMapper = new WorkItemMapper(mappingStrategies); } public override void When() { - Actual = _workItemMapper.Create(SourceWorkItems).SingleOrDefault(); + Actual = WorkItemMapper.Create(SourceWorkItems).SingleOrDefault(); } public override void Cleanup() @@ -87,6 +98,8 @@ public override void Cleanup() } } + + [TestClass] public class Given_a_model_with_a_field_of_type_Double_mapped_from_StringEmpty_Requiring_Conversion : WorkItemMapperContext { @@ -114,6 +127,17 @@ public class EmptyStringModel : IIdentifiable } } + [TestClass] + public class Given_a_model_with_a_field_of_type_Double_mapped_from_StringEmpty_Requiring_Conversion_using_NoExceptionAttributeMapper : + Given_a_model_with_a_field_of_type_Double_mapped_from_StringEmpty_Requiring_Conversion + { + public override void Given() + { + base.Given(); + WorkItemMapper = CreateNoExceptionMapper(); + } + } + [TestClass] public class Given_a_model_with_a_field_of_type_Double_mapped_from_StringEmpty_with_NullSub : WorkItemMapperContext { @@ -364,14 +388,7 @@ private static void PropertiesAreEqual(T expected, T actual) var expectedVal = property.GetValue(expected); var actualVal = property.GetValue(actual); - if (expectedVal is ICollection val) - { - CollectionAssert.AreEqual(val, (ICollection)actualVal); - } - else - { - actualVal.ShouldEqual(expectedVal); - } + actualVal.ShouldEqual(expectedVal, $"{property.Name} ({property.PropertyType.Name}) are not equal."); } } diff --git a/test/Qwiq.Mapper.Tests/packages.config b/test/Qwiq.Mapper.Tests/packages.config index 847287ab..3f277c05 100644 --- a/test/Qwiq.Mapper.Tests/packages.config +++ b/test/Qwiq.Mapper.Tests/packages.config @@ -2,10 +2,10 @@ - + - - + + \ No newline at end of file diff --git a/test/Qwiq.Mocks/MockField.cs b/test/Qwiq.Mocks/MockField.cs index 2f91ecb7..be4ad598 100644 --- a/test/Qwiq.Mocks/MockField.cs +++ b/test/Qwiq.Mocks/MockField.cs @@ -31,16 +31,6 @@ public MockField( IsEditable = true; } - [Obsolete("This method has been deprecated and will be removed in a future version.")] - public MockField( - object value, - object originalValue = null, - ValidationState validationState = ValidationState.Valid, - bool isChangedByUser = true) - : this(MockFieldDefinition.Create(Guid.NewGuid().ToString("N")), value, originalValue, validationState, isChangedByUser) - { - } - internal MockField(IRevisionInternal revision, IFieldDefinition definition) : this(definition) { diff --git a/test/Qwiq.Mocks/MockIdentityManagementService.cs b/test/Qwiq.Mocks/MockIdentityManagementService.cs index 9c7e2f71..ec46f71f 100644 --- a/test/Qwiq.Mocks/MockIdentityManagementService.cs +++ b/test/Qwiq.Mocks/MockIdentityManagementService.cs @@ -9,24 +9,6 @@ namespace Microsoft.Qwiq.Mocks { public class MockIdentityManagementService : IIdentityManagementService { - [Obsolete("This field is depreciated and will be removed in a future version. Use Identities.Adamb instead.")] - public static readonly ITeamFoundationIdentity Adamb = Identities.Adamb; - - [Obsolete("This field is depreciated and will be removed in a future version. Use Identities.Chrisj instead.")] - public static readonly ITeamFoundationIdentity Chrisj = Identities.Chrisj; - - [Obsolete("This field is depreciated and will be removed in a future version. Use Identities.Chrisjoh instead.")] - public static readonly ITeamFoundationIdentity Chrisjoh = Identities.Chrisjoh; - - [Obsolete("This field is depreciated and will be removed in a future version. Use Identities.Chrisjohn instead.")] - public static readonly ITeamFoundationIdentity Chrisjohn = Identities.Chrisjohn; - - [Obsolete("This field is depreciated and will be removed in a future version. Use Identities.Chrisjohns instead.")] - public static readonly ITeamFoundationIdentity Chrisjohns = Identities.Chrisjohns; - - [Obsolete("This field is depreciated and will be removed in a future version. Use Identities.Danj instead.")] - public static readonly ITeamFoundationIdentity Danj = Identities.Danj; - private readonly IDictionary _accountNameMappings; private readonly IDictionary _descriptorMappings; @@ -123,6 +105,21 @@ public IIdentityDescriptor CreateIdentityDescriptor(string identityType, string /// An array of , corresponding 1 to 1 with input descriptor array. /// public IEnumerable ReadIdentities(IEnumerable descriptors) + { + return ReadIdentities(descriptors, MembershipQuery.None); + } + + /// + /// Read identities for given descriptors. + /// + /// + /// Collection of + /// + /// + /// + /// An array of , corresponding 1 to 1 with input descriptor array. + /// + public IEnumerable ReadIdentities(IEnumerable descriptors, MembershipQuery queryMembership) { foreach (var descriptor in descriptors) { @@ -137,6 +134,14 @@ public IEnumerable ReadIdentities(IEnumerable>> ReadIdentities( IdentitySearchFactor searchFactor, IEnumerable searchFactorValues) + { + return ReadIdentities(searchFactor, searchFactorValues, MembershipQuery.None); + } + + public IEnumerable>> ReadIdentities( + IdentitySearchFactor searchFactor, + IEnumerable searchFactorValues, + MembershipQuery queryMembership) { Trace.TraceInformation($"Searching for {searchFactor}: {string.Join(", ", searchFactorValues)}"); @@ -163,10 +168,17 @@ public IEnumerable>> R } } + public ITeamFoundationIdentity ReadIdentity( + IdentitySearchFactor searchFactor, + string searchFactorValue) + { + return ReadIdentity(searchFactor, searchFactorValue, MembershipQuery.None); + } + /// - public ITeamFoundationIdentity ReadIdentity(IdentitySearchFactor searchFactor, string searchFactorValue) + public ITeamFoundationIdentity ReadIdentity(IdentitySearchFactor searchFactor, string searchFactorValue, MembershipQuery queryMembership) { - return ReadIdentities(searchFactor, new[] { searchFactorValue }).First().Value.SingleOrDefault(); + return ReadIdentities(searchFactor, new[] { searchFactorValue }, queryMembership).First().Value.SingleOrDefault(); } private IEnumerable Locate(Func predicate) diff --git a/test/Qwiq.Mocks/MockProject.cs b/test/Qwiq.Mocks/MockProject.cs index 59ad69cc..8e3019d0 100644 --- a/test/Qwiq.Mocks/MockProject.cs +++ b/test/Qwiq.Mocks/MockProject.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; namespace Microsoft.Qwiq.Mocks { @@ -7,56 +6,27 @@ public class MockProject : Project { internal const string ProjectName = "Mock Project"; - public MockProject(Guid id, string name, Uri uri, IWorkItemTypeCollection wits, INodeCollection areas, INodeCollection iterations) + public MockProject(Guid id, string name, Uri uri, IWorkItemTypeCollection wits, IWorkItemClassificationNodeCollection areas, IWorkItemClassificationNodeCollection iterations) : base( id, name, uri, new Lazy(() => wits), - new Lazy(() => areas), - new Lazy(() => iterations)) - { - } - - public MockProject(IWorkItemStore store, INode node) - : base( - Guid.NewGuid(), - node.Name, - node.Uri, - new Lazy(() => new MockWorkItemTypeCollection(store)), - new Lazy(() => CreateNodes(true)), - new Lazy(() => CreateNodes(false))) + new Lazy>(() => areas), + new Lazy>(() => iterations)) { } public MockProject(IWorkItemStore store) - :this(store, new Node(1, false, false, ProjectName, new Uri("http://localhost/projects/1"))) - { - } - - private static INodeCollection CreateNodes(bool area) + : base( + Guid.NewGuid(), + ProjectName, + new Uri("http://localhost/projects/1"), + new Lazy(() => new MockWorkItemTypeCollection(store)), + new Lazy>(() => WorkItemClassificationNodeCollectionBuilder.Build(NodeType.Area)), + new Lazy>(() => WorkItemClassificationNodeCollectionBuilder.Build(NodeType.Iteration)) + ) { - var root = new Node(1, area, !area, "Root", new Uri("http://localhost/nodes/1")); - new Node( - 2, - area, - !area, - "L1", - new Uri("http://localhost/nodes/2"), - () => root, - n => new[] - { - new Node( - 3, - area, - !area, - "L2", - new Uri("http://localhost/nodes/3"), - () => n, - c => Enumerable.Empty()) - }); - - return new NodeCollection(new[] { root }.ToList().AsReadOnly()); } } } \ No newline at end of file diff --git a/test/Qwiq.Mocks/MockTeamFoundationIdentity.cs b/test/Qwiq.Mocks/MockTeamFoundationIdentity.cs index daac88f1..03d118c9 100644 --- a/test/Qwiq.Mocks/MockTeamFoundationIdentity.cs +++ b/test/Qwiq.Mocks/MockTeamFoundationIdentity.cs @@ -1,8 +1,7 @@ +using Microsoft.VisualStudio.Services.Common; using System; using System.Collections.Generic; -using Microsoft.VisualStudio.Services.Common; - namespace Microsoft.Qwiq.Mocks { public class MockTeamFoundationIdentity : TeamFoundationIdentity @@ -16,12 +15,15 @@ public MockTeamFoundationIdentity( bool isActive = true, IEnumerable members = null, IEnumerable memberOf = null) - : base(isActive, teamFoundationId == Guid.Empty ? Guid.NewGuid() : teamFoundationId, isActive ? 0 : 1) + : base( + isActive, + teamFoundationId == Guid.Empty ? Guid.NewGuid() : teamFoundationId, + isActive ? 0 : 1, + memberOf ?? ZeroLengthArrayOfIdentityDescriptor, + members ?? ZeroLengthArrayOfIdentityDescriptor) { DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName)); - MemberOf = memberOf ?? ZeroLengthArrayOfIdentityDescriptor; - Members = members ?? ZeroLengthArrayOfIdentityDescriptor; IsContainer = false; Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor)); @@ -40,22 +42,12 @@ public MockTeamFoundationIdentity( }; } - [Obsolete("This method has been deprecated and will be removed in a future release.")] - public MockTeamFoundationIdentity(string displayName, string uniqueName) - : this(MockIdentityDescriptor.Create(uniqueName), displayName, Guid.Empty) - { - } - public sealed override IIdentityDescriptor Descriptor { get; } public sealed override string DisplayName { get; } public sealed override bool IsContainer { get; } - public sealed override IEnumerable MemberOf { get; } - - public sealed override IEnumerable Members { get; } - public override string GetAttribute(string name, string defaultValue) { return _properties.TryGetValue(name, out object obj) diff --git a/test/Qwiq.Mocks/MockWorkItem.cs b/test/Qwiq.Mocks/MockWorkItem.cs index a59b69f5..31bcb7bf 100644 --- a/test/Qwiq.Mocks/MockWorkItem.cs +++ b/test/Qwiq.Mocks/MockWorkItem.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.Contracts; using System.Linq; using System.Threading; @@ -23,43 +22,6 @@ public class MockWorkItem : WorkItem, IWorkItem private int _tempId; - [DebuggerStepThrough] - [Obsolete( - "This method has been deprecated and will be removed in a future release. See a constructor that takes IWorkItemType and fields.")] - public MockWorkItem() - : this("Mock") - { - } - - [DebuggerStepThrough] - [Obsolete( - "This method has been deprecated and will be removed in a future release. See a constructor that takes IWorkItemType and fields.")] - public MockWorkItem(string workItemType = null) - : this(workItemType, (IDictionary)null) - { - } - - [DebuggerStepThrough] - [Obsolete( - "This method has been deprecated and will be removed in a future release. See a constructor that takes IWorkItemType and fields.")] - public MockWorkItem(IDictionary fields) - : this((string)null, fields) - { - } - - [Obsolete( - "This method has been deprecated and will be removed in a future release. See a constructor that takes IWorkItemType and fields.")] - public MockWorkItem(string workItemType = null, IDictionary fields = null) - : this( - new MockWorkItemType( - workItemType ?? "Mock", - CoreFieldDefinitions.All.Union( - fields?.Keys.Select(MockFieldDefinition.Create) - ?? Enumerable.Empty())), - (Dictionary)fields) - { - } - public MockWorkItem([CanBeNull] string workItemType, [CanBeNull] params IField[] fields) : this(new MockWorkItemType(workItemType ?? "Mock", CoreFieldDefinitions.All.Union(fields.Select(f=>f.FieldDefinition)))) { @@ -76,8 +38,6 @@ public MockWorkItem([CanBeNull] string workItemType, [CanBeNull] params IField[] f.Revision = this; SetFieldValue(field.FieldDefinition, val); } - - } } } @@ -133,11 +93,6 @@ private static Dictionary NormalizeFields(IWorkItemType type, Di return retval; } - public override IRelatedLink CreateRelatedLink(IWorkItemLinkTypeEnd linkTypeEnd, IWorkItem relatedWorkItem) - { - return CreateRelatedLink(relatedWorkItem.Id, linkTypeEnd); - } - public override IRelatedLink CreateRelatedLink(int id, IWorkItemLinkTypeEnd linkTypeEnd = null) { if (IsNew) throw new InvalidOperationException("Save first"); @@ -185,12 +140,6 @@ public override bool IsDirty } } - public override string Keywords - { - get => GetValue(WorkItemFields.Keywords); - set => SetValue(WorkItemFields.Keywords, value); - } - public new ICollection Links { get; set; } public new int RelatedLinkCount => Links.OfType().Count(); @@ -210,9 +159,7 @@ public override string Keywords } } - public override Uri Uri => new Uri($"vstfs:///WorkItemTracking/WorkItem/{Id}"); - - public override string Url => Uri.ToString(); + public override string Url => $"vstfs:///WorkItemTracking/WorkItem/{Id}"; public override void ApplyRules(bool doNotUpdateChangedBy = false) { @@ -266,10 +213,10 @@ public override IWorkItem Copy() } // Our limitation: need an ID to link so save before we do anything else target.Id = target._tempId; - target.Links.Add(target.CreateRelatedLink(e.ForwardEnd, this)); + target.Links.Add(target.CreateRelatedLink(Id, e.ForwardEnd)); // Recipricol links are typically handled in Save operation - Links.Add(CreateRelatedLink(e.ReverseEnd, target)); + Links.Add(CreateRelatedLink(target.Id, e.ReverseEnd)); target.Id = 0; } @@ -337,25 +284,6 @@ public override IEnumerable Validate() var invalidFields = Fields.Where(p => !p.IsValid).Select(p => p).ToArray(); return invalidFields.Any() ? invalidFields : null; } - - [Obsolete( - "This method is deprecated and will be removed in a future version. See CreateRelatedLink(IWorkItemLinkTypeEnd, IWorkItem) instead.")] - public IRelatedLink CreateRelatedLink(IWorkItem target) - { - var store = Type.Store(); - if (store != null) - { - var e = store.WorkItemLinkTypes[CoreLinkTypeReferenceNames.Related].ForwardEnd; - return CreateRelatedLink(e, target); - } - - using (var wis = new MockWorkItemStore()) - { - return CreateRelatedLink( - wis.WorkItemLinkTypes[CoreLinkTypeReferenceNames.Related].ForwardEnd, - target); - } - } } diff --git a/test/Qwiq.Mocks/MockWorkItemStore.cs b/test/Qwiq.Mocks/MockWorkItemStore.cs index 981ffb3c..6fdc372b 100644 --- a/test/Qwiq.Mocks/MockWorkItemStore.cs +++ b/test/Qwiq.Mocks/MockWorkItemStore.cs @@ -25,14 +25,6 @@ public MockWorkItemStore() { } - [Obsolete("This method has been deprecated and will be removed in a future release.")] - public MockWorkItemStore(IEnumerable workItems, IEnumerable links = null) - : this() - - { - this.Add(workItems, links); - } - public MockWorkItemStore([InstantHandle] [NotNull] Func tpcFactory, [InstantHandle] [NotNull] Func queryFactory) { if (tpcFactory == null) throw new ArgumentNullException(nameof(tpcFactory)); diff --git a/test/Qwiq.Mocks/MockWorkItemType.cs b/test/Qwiq.Mocks/MockWorkItemType.cs index a47c45ae..a18d40a7 100644 --- a/test/Qwiq.Mocks/MockWorkItemType.cs +++ b/test/Qwiq.Mocks/MockWorkItemType.cs @@ -7,13 +7,6 @@ namespace Microsoft.Qwiq.Mocks { public class MockWorkItemType : WorkItemType { - [DebuggerStepThrough] - [Obsolete("This method has been deprecated and will be removed in a future release. See ctor(IWorkItemStore, String, String).")] - public MockWorkItemType() - : this("Mock") - { - } - [DebuggerStepThrough] public MockWorkItemType(string name, string description = null, IWorkItemStore store = null) : this(name, new MockFieldDefinitionCollection(CoreFieldDefinitions.All), description, store) diff --git a/test/Qwiq.Mocks/Qwiq.Mocks.csproj b/test/Qwiq.Mocks/Qwiq.Mocks.csproj index 34a57135..4e23204a 100644 --- a/test/Qwiq.Mocks/Qwiq.Mocks.csproj +++ b/test/Qwiq.Mocks/Qwiq.Mocks.csproj @@ -1,6 +1,6 @@  - + @@ -73,6 +73,7 @@ + @@ -97,8 +98,8 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + \ No newline at end of file diff --git a/test/Qwiq.Mocks/WorkItemClassificationNodeCollectionBuilder.cs b/test/Qwiq.Mocks/WorkItemClassificationNodeCollectionBuilder.cs new file mode 100644 index 00000000..f41bf1e5 --- /dev/null +++ b/test/Qwiq.Mocks/WorkItemClassificationNodeCollectionBuilder.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Qwiq.Mocks +{ + internal static class WorkItemClassificationNodeCollectionBuilder + { + public static IWorkItemClassificationNodeCollection Build(NodeType type) + { + return new WorkItemClassificationNodeCollection(BuildNode(type)); + } + + private static IEnumerable> BuildNode(NodeType type) + { + string path = "\\"; + for (int j = 1; j < 4; j++) + { + path += j + "\\"; + yield return new WorkItemClassificationNode(j, type, path, new Uri("http://localhost/nodes/" + j)); + } + } + } +} \ No newline at end of file diff --git a/test/Qwiq.Mocks/packages.config b/test/Qwiq.Mocks/packages.config index 7f3d2b81..2a77675c 100644 --- a/test/Qwiq.Mocks/packages.config +++ b/test/Qwiq.Mocks/packages.config @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/test/Qwiq.Tests.Common/Qwiq.Tests.Common.csproj b/test/Qwiq.Tests.Common/Qwiq.Tests.Common.csproj index 86e695f4..eed25c3e 100644 --- a/test/Qwiq.Tests.Common/Qwiq.Tests.Common.csproj +++ b/test/Qwiq.Tests.Common/Qwiq.Tests.Common.csproj @@ -1,7 +1,7 @@  - - + + @@ -21,10 +21,10 @@ ..\..\packages\Humanizer.Core.2.1.0\lib\netstandard1.0\Humanizer.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll - ..\..\packages\MSTest.TestFramework.1.1.17\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + ..\..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll ..\..\packages\Should.1.1.20\lib\Should.dll @@ -60,11 +60,11 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - + + + - + \ No newline at end of file diff --git a/test/Qwiq.Tests.Common/packages.config b/test/Qwiq.Tests.Common/packages.config index 097a5d0a..5c9fed27 100644 --- a/test/Qwiq.Tests.Common/packages.config +++ b/test/Qwiq.Tests.Common/packages.config @@ -2,8 +2,8 @@ - - - + + + \ No newline at end of file