lokendra jayaswal
lokendra jayaswal

Reputation: 308

Distinct Dictionary Using LINQ

I am trying to get the dictionary of distinct values using LINQ. I have tried using this:

var roleRefList = 
    xDocument.Root.Descendants()
             .Where(x => x.Name.LocalName.Equals("roleRef") && 
                         !string.IsNullOrEmpty(Convert.ToString(x.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("roleURI")))) && 
                         !string.IsNullOrEmpty(Convert.ToString(x.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("href")))))
             .Select(l => new {
                  roleUri = l.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("roleURI")).Value,
                  href = l.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("href")).Value
              })
             .Distinct()
             .ToDictionary(a => a.roleUri);

The problem here is that when there are duplicate entries in the roleUri then an error occurs. I am parsing the XML document and making a dictionary of xElement attributes roleUri and roleref if they are present in the xElement.

The other workaround is using a for loop:

Dictionary<string, string> roleRefList = new Dictionary<string, string>();
            foreach (XElement element in xDocument.Root.Descendants().Where(x => x.Name.LocalName.Equals("roleRef")))
            {
                string roelUri = Convert.ToString(element.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("roleURI")));
                string href = Convert.ToString(element.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("href")));
                if (!string.IsNullOrEmpty(roelUri) && !string.IsNullOrEmpty(href) && !roleRefList.ContainsKey(roelUri))
                {
                    roleRefList.Add(roelUri, href);
                }
            }

but I want to implement this using LINQ.

Upvotes: 4

Views: 8291

Answers (3)

Sergey Berezovskiy
Sergey Berezovskiy

Reputation: 236228

Duplicate attributes are not allowed in XML. If you will have two roleURI attributes in roleRef element then you will get exception during XDocument loading:

'roleURI' is a duplicate attribute name. Line 42, position 42.

So, actually your code should look like this:

var xdoc = XDocument.Load("foo.xml");
XNamespace ns = "http://www.adventure-works.com"; // put your namespace here

Dictionary<string, string> roleRefList = 
   xdoc.Root.Descendants(ns + "roleRef")
       .Select(r => new {
             Uri = (string)r.Attribute("roleURI"),
             Href = (string)r.Attribute("href")
       })
       .Where(r => !String.IsNullOrEmpty(r.Uri) && !String.IsNullOrEmpty(r.Href))
       .ToDictionary(r => r.Uri, r => r.Href);

Result will be same as with your for loop. Sample xml:

<root xmlns="http://www.adventure-works.com">
  <roleRef/>
  <roleRef roleURI=""/>
  <roleRef href=""/>
  <roleRef roleURI="" href=""/>
  <roleRef roleURI="a" />
  <roleRef roleURI="" href="b"/>
  <roleRef roleURI="c" href="d"/>
</root>

Upvotes: 1

TKharaishvili
TKharaishvili

Reputation: 2099

Couple of ways to achieve this:

1) You can call the distinct method using a custom equality comparer. In order to achieve this you will need to write a class which will contain the roleUri and href fields first. Something like:

    public class AttributePair 
    {
      public string RoleUri {get; set;}
      public string Href {get; set;}
    }

Next step is writing the equality comparer for your class:

    public class AttributePairComparer : IEqualityComparer<AttributePair>
    {
        public bool Equals(AttributePair x, AttributePair y)
        {
            return x.RoleUri.Equals(y.RoleUri);
        }

        public int GetHashCode(AttributePair obj)
        {
            return obj.RoleUri.GetHashCode();
        }
    }

And after that we'll just pass the instance of the equality comparer to the distinct method:

 var roleRefList = xDocument.Root.Descendants().Where(x => x.Name.LocalName.Equals("roleRef") && !string.IsNullOrEmpty(Convert.ToString(x.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("roleURI")))) && !string.IsNullOrEmpty(Convert.ToString(x.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("href")))))
                       .Select(l => new AttributePair
                       {
                           RoleUri = l.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("roleURI")).Value,
                           Href = l.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("href")).Value
                       }).Distinct(new AttributePairComparer()).ToDictionary(a => a.RoleUri, a => a.Href);

I agree that this solution is quite complicated, but this is a classic way of achieving the needed result, so to speak.

2) Another solution is a DistinctBy method from MoreLinq. Where you can pass a lambda expression as a parameter:

 var roleRefList = xDocument.Root.Descendants().Where(x => x.Name.LocalName.Equals("roleRef") && !string.IsNullOrEmpty(Convert.ToString(x.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("roleURI")))) && !string.IsNullOrEmpty(Convert.ToString(x.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("href")))))
                       .Select(l => new
                       {
                           roleUri = l.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("roleURI")).Value,
                           href = l.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("href")).Value
                       }).DistinctBy(p => p.roleUri).ToDictionary(a => a.roleUri, a => a.href);

3) The chief architect of C#, Anders Hejlsberg, suggested the solution which uses GroupBy. You can read about it here.

Upvotes: 0

MarcinJuraszek
MarcinJuraszek

Reputation: 125630

You could write your own Distinct method that would take Func<T,TKey> as an argument. You can find example of that here: Distinct list of objects based on an arbitrary key in LINQ

With that method you should be able to write:

var roleRefList = xDocument.Root.Descendants().Where(x => x.Name.LocalName.Equals("roleRef") && !string.IsNullOrEmpty(Convert.ToString(x.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("roleURI")))) && !string.IsNullOrEmpty(Convert.ToString(x.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("href")))))
                           .Select(l => new
                           {
                               roleUri = l.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("roleURI")).Value,
                               href = l.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("href")).Value
                           }).Distinct(l => l.roleUri).ToDictionary(a => a.roleUri);

Update

Or you can use grouping:

var roleRefList = xDocument.Root.Descendants().Where(x => x.Name.LocalName.Equals("roleRef") && !string.IsNullOrEmpty(Convert.ToString(x.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("roleURI")))) && !string.IsNullOrEmpty(Convert.ToString(x.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("href")))))
                           .Select(l => new
                           {
                               roleUri = l.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("roleURI")).Value,
                               href = l.Attributes().FirstOrDefault(a => a.Name.LocalName.Equals("href")).Value
                           })
                           .GroupBy(l => l.roleUri)
                           .ToDictionary(g => g.Key, g => g.FirstOrDefault());

Upvotes: 9

Related Questions