Click here to Skip to main content
Click here to Skip to main content

Associating Attribute Meta-Data in a Peer Graph using Microsoft's Peer-to-Peer technology

, 6 Jan 2006
Rate this:
Please Sign up or sign in to vote.
Associating attribute meta-data in a Peer Graph using Microsoft's Peer-to-Peer technology.

Background

Microsoft's Peer-to-Peer Graphing technology provides a stable, reliable, and robust infrastructure for Windows peer-to-peer applications to communicate. Peers use the Peer Name Resolution Protocol (PNRP - a serverless DNS) to register and discover other peers within the graph. Graphs are the foundation for connecting peers, services, and resources within a peer network. A peer can be a user-interactive application, service, or resource. Graphing allows data to be passed between peers efficiently and reliably.

Microsoft's entire Peer-to-Peer technology is exposed through the latest Platform SDK as C/C++ API calls. However, the code in this article shows these APIs being used from .NET managed code using C#.

Introduction

This article introduces the concept of associating meta-data with peer records within a peer-to-peer graph. As we learned in the previous article, a record could represent the presence of a service, the availability of the CPU to do more work, the status of a resource that is updated periodically, or the URL of your latest blog entry. To simplify searching, rather than bury this information in the data of the record, meta-data can be attached to each record. In Microsoft terms, this meta-data is called attributes. Attributes are represented as XML in order to allow a wider range of capabilities.

Microsoft provides a simple schema against which the attribute XML fragments must conform. The low level PeerGraph APIs treat this XML fragment as a simple string. This article focuses on a PeerAttributes helper class for managing attributes and providing methods to convert this information to and from the required XML format. It also describes an update to the PeerRecord class now that attribute support has been implemented.

XML Schema

The XML schema is very simple. The root element must be called <attributes>. It contains sub-elements that must be called <attribute>. Each <attribute> tag has two XML attributes; name and type. name represents the name of the attribute, and type its type. Only three data types are supported; int, date, and string. The value is specified between the beginning and ending tags. Here is a simple example:

<attributes>
  <attribute name="Type" type="string">
      4d5b2f11-6522-433b-84ef-a298e60757b0
  </attribute>
  <attribute name="CreationTime" type="date">
      2005-12-31T17:00:00.0000000-07:00
  </attribute>
</attributes>

The Attributes.xsd file included in the sample code contains the schema. It's copied from the MSDN library web page that describes the Record Attribute Schema. This schema is used by the Validate method described below.

PeerAttribute Class

The PeerAttribute class is a simple wrapper for the storage of an attribute. The class contains three properties; name which is read-only, type and value which are both updatable.

public enum PeerAttributeType
{
  Int,
  String,
  Date
}

public class PeerAttribute
{
  private string name;
  private PeerAttributeType type;
  private object data;

  public PeerAttribute(string Name, PeerAttributeType Type, object Value)
  {
    name = Name;
    type = Type;
    data = Value;
  }

  public string Name
  {
    get { return name; }
  }

  public PeerAttributeType Type
  {
    get { return type; }
    set { type = value; }
  }

  public object Value
  {
    get { return data; }
    set { data = value; }
  }
}

PeerAttributes Collection

The PeerAttribues class is derived from CollectionBase to provide convenient storage for PeerAttribute objects and standard collection methods. Beyond the methods described below, it includes an indexer (Item[int] and Item[string]), and overrides the ToString method to return the XML equivalent of the attributes stored in the collection.

Simple Add Methods

Four methods are provided to add a single attribute at a time to the collection.

private void Add(string Name, PeerAttributeType Type, object Value)
{
  this.InnerList.Add(new PeerAttribute(Name, Type, Value));
}

public void Add(string Name, int Value)
{
  Add(Name, PeerAttributeType.Int, Value);
}

public void Add(string Name, DateTime Value)
{
  Add(Name, PeerAttributeType.Date, Value);
}

public void Add(string Name, string Value)
{
  Add(Name, PeerAttributeType.String, Value);
}

The first method creates a PeerAttribute object with the given name, type, and value. The other three methods are type specific versions which call the first method.

Add Object Method

A typical peer-to-peer application will use its own run-time objects to represent the meta data associated with a node or record being shared. The remaining Add method allows the developer to pass this run-time object, and using reflection, extract the current values of its properties into attributes. The sample application extracts the properties from a System.IO.FileInfo object.

public void Add(object Instance)
{
  foreach (PropertyInfo prop in Instance.GetType().GetProperties())
  {
    try
    {
      object oValue = prop.GetValue(Instance, null);

      if (prop.PropertyType.FullName == "System.Int32")
        Add(prop.Name, (int)oValue);
      else if (prop.PropertyType.FullName == "System.DateTime")
        Add(prop.Name, (DateTime)oValue);
      else
        Add(prop.Name, oValue.ToString());
    }
    catch (Exception ex) 
    {
      // ignore exceptions
    }
  }
}

For each property the run-time object exposes, its current value and type are extracted to create a PeerAttribute object.

Remove Method

In some cases, the Add Object method may extract properties that the developer does not want to expose as meta-data. The Remove method allows unwanted attributes to be removed by name.

public void Remove(string Name)
{
  int i = 0;
  foreach (PeerAttribute attr in this.List)
  {
    if (attr.Name == Name)
    {
      this.List.RemoveAt(i);
      break;
    }
    i++;
  }
}

XML Property

The get part of the XML property converts the attributes stored in the collection into an XML fragment using the XmlTextWriter class.

get
{
  if (this.List.Count == 0) return null;

  XmlTextWriter xt = new XmlTextWriter(new MemoryStream(), 
                     System.Text.UTF8Encoding.Unicode);
  xt.Formatting = Formatting.Indented;
  xt.WriteStartElement("attributes");
  foreach (PeerAttribute attr in this.List)
  {
    xt.WriteStartElement("attribute");
    xt.WriteAttributeString("name", attr.Name);
    xt.WriteAttributeString("type", StringFromType(attr.Type));
    string s = attr.Value.ToString();
    if (attr.Type == PeerAttributeType.Date)
      s = XmlConvert.ToString(Convert.ToDateTime(attr.Value));
    xt.WriteString(s);
    xt.WriteEndElement();
  }
  xt.WriteEndElement();
  xt.Flush();
  xt.BaseStream.Position = 0;
  return new StreamReader(xt.BaseStream).ReadToEnd();
}

The set part of the XML property converts each <attribute> element of the XML fragment and stores them as PeerAttribute objects in the collection. The XmlTextReader is used to identify each element for conversion.

set
{
  if (value == null || value == string.Empty) return;
  this.Clear();

  XmlTextReader xr = new XmlTextReader(new StringReader(value));

  string Name = string.Empty;
  PeerAttributeType Type = PeerAttributeType.String;
  object Value = null;

  while (xr.Read())
  {
    if (xr.NodeType == XmlNodeType.Element && 
        xr.Name == "attribute")
    {
      xr.MoveToFirstAttribute();
      int count = xr.AttributeCount;
      for (int i = 0; i < count; i++)
      {
        xr.MoveToAttribute(i);
        if (xr.Name == "name")
        {
          xr.ReadAttributeValue();
          Name = xr.Value;
        }
        else if (xr.Name == "type")
        {
          xr.ReadAttributeValue();
          Type = TypeFromString(xr.Value);
        }
      }
    }
    else if (xr.NodeType == XmlNodeType.Text)
    {
      Value = xr.Value;
    }
    else if (xr.NodeType == XmlNodeType.EndElement && 
             xr.Name == "attribute")
    {
      switch (Type)
      {
      case PeerAttributeType.Int:
        Add(Name, Convert.ToInt32(Value));
        break;
      case PeerAttributeType.Date:
        Add(Name, Convert.ToDateTime(Value));
        break;
      default:
        Add(Name, Value.ToString());
        break;
      }
    }
  }
}

The first if-block handles extracting the name and type attributes. The middle if-block handles extracting the value as text. The last if-block uses the information gathered from the previous two if-blocks to call the appropriate Add method and perform the correct data type conversion of the value.

Validate Method

The sample file Attributes.xsd contains the schema for validating XML attribute fragments and is stored as an embedded resource in the sample application. This schema is loaded and compiled as a static (shared) field of the PeerAttributes collection. The Validate method uses the schema to validate the XML fragment equivalent to the current attributes stored in the collection.

public bool Validate()
{
  XmlValidatingReader vr = new XmlValidatingReader(Xml, 
                               XmlNodeType.Element, null);
  vr.Schemas.Add(schema,null);
  bool valid;
  try
  {
    while (vr.Read()) { }
    valid = true;
  }
  catch (XmlSchemaException ex)
  {
    valid = false;
  }
  catch (XmlException ex)
  {
    valid = false;
  }
  return valid;
}

The Validate method returns true if the XML fragment matches the schema, otherwise, false if an exception is thrown. There should never be a need to validate the XML fragment, but this feature is included for completeness.

Update to PeerRecord

The source code from a previous article had attributes of the PeerRecord class exposed as a string property. In this article, the source code has been updated to expose a read-only property that returns a PeerAttributes collection.

private PeerAttributes attributes;
public PeerAttributes Attributes
{
  get { return attributes; }
}

Attributes are now automatically converted to or from the XML string fragment as the record is marshaled for use with the underlying API methods.

Using the Sample Application

The sample application lets you first create a graph (unsecured peer name 0.SharedFiles) with an initial identity. The first instance should be opened with this identity. It will pause a few seconds looking for other instances, then begin to listen. Each subsequent instance of the application should open the graph with a different identity. These instances will connect to the nearest peer and synchronize. Each instance of the application is a peer.

Use the Add Folder button to select a folder containing files.

private void Add_Click(object sender, System.EventArgs e)
{
  DialogResult result = folderBrowserDialog1.ShowDialog();
  if (result == DialogResult.OK)
  {
    string path = folderBrowserDialog1.SelectedPath;
    DirectoryInfo dinfo = new DirectoryInfo(path);
    // publish each file as a record
    foreach (FileInfo info in dinfo.GetFiles())
    {
      // note the record will expire after 1 minute
      PeerRecord record = graph.CreatePeerRecord(FILE_RECORD_TYPE, 
                                             new TimeSpan(0,1,0));
      record.Attributes.Add(info);
      record.DataAsString = info.Name;
      Guid recordId = graph.AddRecord(record);
    }
  }
}

After selecting a folder, each file in the folder is published to the graph as a record. Note that the record is set to expire after 1 minute. The attributes of the record are set to the properties of the FileInfo class. The data of the record is set to the name of the file.

private void OnRecordChanged(object sender, PeerGraphRecordChangedEventArgs e)
{
  PeerRecord record;
  FileItem item;
  switch (e.Action)
  {
  case PeerRecordAction.Added:
    record = graph.GetRecord(e.RecordId);
    LogMessage(@"Added", record.DataAsString);
    item = new FileItem(record.DataAsString, e.RecordId);
    listBox2.Items.Add(item);
    break;
...

After a record is added, the RecordChanged event fires with the action set to Added. The PeerRecord corresponding to the RecordID added and the record data containing the file name are used to create a simple FileItem object that associates the name with the record ID in the list.

public class FileItem
{
  private string name;
  private Guid recordid;
  public FileItem(string Name, Guid RecordId)
  {
    name = Name;
    recordid = RecordId;
  }

  public string Name
  {
    get { return name; }
  }

  public Guid RecordId
  {
    get { return recordid; }
  }
}

Click on a file name in the list box to show the XML attributes associated with the record in the right text box. The PeerAttributes XML property is used to read the attributes as an XML fragment.

private void listBox2_SelectedIndexChanged(object sender, System.EventArgs e)
{
  if (listBox2.SelectedIndex == -1)
    textBox3.Text = string.Empty;
  else
  {
    FileItem item = (FileItem)listBox2.SelectedItem;
    PeerRecord record = graph.GetRecord(item.RecordId);
    textBox3.Text = record.Attributes.Xml;
  }
}

The lower list shows a diagnostic log of all actions and incoming events. Double-click to clear the list.

Points of Interest

While implementing the helper class, I ran across a bug / limitation in the attribute's ability to handle dates.

<attributes>
  <attribute name="CreationTime" 
        type="date">1600-12-31T17:00:00.0000000-07:00</attribute>
</attributes>

It seems, a year any earlier than 1754 will cause the PeerGraphAddRecord or PeerGraphUpdateRecord methods to throw a 0x80040E21 exception. After some investigation, I discovered a Microsoft Knowledge Base article that explains how the Access ODBC Driver Cannot Insert Dates Prior to the Year 1753. It would appear that the underlying storage mechanism that peer-to-peer graphing uses is based on the Jet engine.

Links to Resources

I have found the following resources to be very useful in understanding peer graphs:

Conclusion

I hope you have found this article useful. The next article will focus on searching for records matching information located in the meta-data. Stay tuned for more articles on the following topics:

  1. Peer Name Resolution - Windows Vista Enhancements
  1. Peer Graph - Searching
  2. Peer Graph - Importing and Exporting a Database
  1. Peer Groups and Identity
  1. Peer Collaboration - People Near Me
  2. Peer Collaboration - EndPoints
  3. Peer Collaboration - Capabilities
  4. Peer Collaboration - Presence
  5. Peer Collaboration - Invitations

If you have suggestions for other topics, please leave a comment.

History

  • Initial revision.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Adrian_Moore
Web Developer
Canada Canada
Adrian Moore is the Development Manager for the SCADA Vision system developed by ABB Inc in Calgary, Alberta.
 
He has been interested in compilers, parsers, real-time database systems and peer-to-peer solutions since the early 90's. In his spare time, he is currently working on a SQL parser for querying .NET DataSets (http://www.queryadataset.com).
 
Adrian is a Microsoft MVP for Windows Networking.

Comments and Discussions

 
GeneralTrouble in subsequent instance [modified] Pinmember_Maverick_16-Apr-07 19:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.1411023.1 | Last Updated 6 Jan 2006
Article Copyright 2006 by Adrian_Moore
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid