Integrated social networking in ASP.NET
This riff is on the concept of baking social networking structures into our tools so that we don't have to re-invent the wheel every time we build an application that wants to do things like tagging, commenting, and communicating with a mash-up API.
Similar to ASP.NET Role, Profile and Membership providers, we should be able to opt in to these features, perhaps in a separate application-agnostic social data store, and be done with it. Here's an example of what I mean.
Using tagging as a simple example, we're talking about a many-tags-to-many-entities relationship where we can effectively tag anything. Our business validation probably includes ensuring that the tag is case insensitive and displayed uniformly in lowercase, that we either create a new tag or retrieve an old one when challenged with a new tag request, and that we trim any leading and trailing whitespace to preserve data integrity. The ORM model could look something like this:
All we're doing here is associating any tag to any entity that is uniquely identifiable. We have a TagType just in case we need to distinguish tags in the future, but that isn't necessary. Let's come up with a way to make use of generic tagging in a separate web application.
using Dimebrain.Biz.Social.Attributes;using Dimebrain.Biz.Social.Entities;namespace Dimebrain.Demo.Data.Entities{[Taggable]
public partial class Customer : ISocialEntity
{}
}
In the code listing above, we're off in our own specific web application referencing a few namespaces that include an attribute and an interface. The 'Taggable' attribute serves to provide the class with some extension methods at runtime that let us tag customers. To do that, we can use a custom base class within the helper assembly to parse the attributes and provide details to the extension methods:
using System;using System.Collections.Generic;using Dimebrain.Biz.Social.Attributes;using Dimebrain.Data;using Dimebrain.Extensions;namespace Dimebrain.Biz.Social.Entities{public class SocialEntity : EntityBase, ISocialEntity
{public virtual Guid Id { get; set; }
public virtual bool IsTaggable { get; private set; }
public virtual bool IsCommentable { get; private set; }
private ISocialEntity _entity;public virtual ISocialEntity Entity
{ set { _entity = value; }}
public SocialEntity() { //}
public SocialEntity(ISocialEntity entity) {_entity = entity;
ParseAttributes();
}
private void ParseAttributes()
{var T = _entity.GetType();
var attributes = new List<AttributeBase>();T.GetCustomAttributes(true).ForEach(c => attributes.Add(c as AttributeBase));
attributes.ForEach(a => IsTaggable |= a is TaggableAttribute, a => IsCommentable |= a is CommentableAttribute);}
}
}
All that's required now is to add a Taggable attribute to a class, implement ISocialEntity (which requires a GUID-based identifier), and we can tag the entity in our social data store using the Tag extension method.
An important consideration for setting a system like this up is just in how you want to reference the social data in the application you're attempting to socialize. For this to work well, we don't want to get in the way of the developer trying to use its, so it's absolutely necessary to avoid asking them to use a base class for their entities. While in this example we are still enforcing a GUID-based identifier in order to associate our tag with an entity, we need to do that with an interface, not a base class, and get out of the way. We can definitely take this concept further (and we will), but for now this is a workable approach. We haven't spent any time dealing with persistence ignorance (in fact we're doing the opposite by pushing a GUID on the user) or inversion of control, nevermind LINQ to SQL's deep entity namespace dependencies (in other words we haven't created sufficient wrappers so our custom application needs to reference the data layer of our helper components just to get access to the extension methods, but hopefully you'll see this as the start of something rather than the end).
using System;using Dimebrain.Biz.Social.Entities;namespace Dimebrain.Biz.Extensions{public static class Extensions
{public static void Tag(this ISocialEntity entity, string tag)
{ var instance = new SocialEntity(entity) { Id = entity.Id }; if(instance.IsTaggable) {Facade.AddTagToEntity(Facade.CreateOrFetchTag(tag).Id, entity.Id);
}
else {throw new MethodAccessException("Cannot tag an entity that is not declared as taggable.");
}
}
public static void Comment(this ISocialEntity entity, string comment)
{ // Persist to the repository}
}
}
In the extension method implementation we'll take the ISocialEntity interface from the user and, since we really only have one entity to map to a concrete class, we can just do that explicitly, creating the concrete instance in the method to wrap the interface; at constructor time, the entity instance is parsed for its attributes. If it passes the 'taggable' test, we open up our internal business layer to perform the tagging task; if it doesn't, we'll let the developer know that they've missed an important declaration in their object model.
From here, there's not much else to it. The tagging story mentioned earlier is realized in the business layer, and we can now decorate our POCO objects with an interface and an attribute to provide social tagging "out of the box", and call a simple extension method to do the heavy lifting.
using System;using System.Data.Linq;using System.Linq;using Dimebrain.Data;using Dimebrain.Data.Entities;namespace Dimebrain.Biz{public class Facade
{public static Tag CreateOrFetchTag(string tag)
{var db = Database.Context;
var text = tag.Trim().ToLower();
var fetch = db.Tags.Where(t => t.Name == text).SingleOrDefault();
return fetch ?? CreateTag(text);}
private static Tag CreateTag(string tag)
{var db = Database.Context;
var text = tag.Trim().ToLower();
var entity = new Tag{Name = text};db.Tags.InsertOnSubmit(entity);
db.SubmitChanges(ConflictMode.FailOnFirstConflict);
return entity;}
public static bool AddTagToEntity(Guid tagId, Guid entityId)
{var db = Database.Context;
var map = db.EntityTags.Where(et => et.EntityId == entityId && et.TagId == tagId).SingleOrDefault();
if (map == null)
{ map = new EntityTag{EntityId = entityId, TagId = tagId};db.EntityTags.InsertOnSubmit(map);
db.SubmitChanges(ConflictMode.FailOnFirstConflict);
return true;
}
return false;
}
public static bool RemoveTagFromEntity(Guid tagId, Guid entityId)
{var db = Database.Context;
var map = db.EntityTags.Where(et => et.EntityId == entityId && et.TagId == tagId).SingleOrDefault();
if (map == null)
{return false;
}
db.EntityTags.DeleteOnSubmit(map);
db.SubmitChanges(ConflictMode.FailOnFirstConflict);
return true;
}
}
}
Social networking features are a good candidate to suffer from repetition, but we have many options to contain all of them in reusable components.










Creating a provider model around tagging that is similar to the ASP.NET Membership API would be very useful when needing to integrate tagging functionality within an application. This is a really cool idea!
Let me know if you grown this out into something more, like an open source project.
Posted by: Chris Pietschmann | May 21, 2008 at 06:31 PM
@Chris,
Will do; the source for this sample is part of the CodePlex project as always, but I am working hard on a more thorough treatment of this; I'll let you know when there's a reference implementation.
Posted by: Daniel | May 22, 2008 at 12:41 AM
I've been experimenting with ASP.NET to create social networking features. I created a tag cloud from data pulled from the YouTube API and an improved method of displaying comment threads. I'm currently working with the Seesmic API and found ASP.NET a bit lacking in JSON support. I wanted to deserialize JSON into an object without knowing the data structure. I had to request XML instead because it is easier to work with when you are not consuming the JSON with JavaScript.
Posted by: Robert S. Robbins | May 22, 2008 at 02:51 PM
@Robert,
So I'm clear, you're making a web request that returns a JSON string and you want to deserialize that into an object where you haven't previously created a concrete class to store it? You might want to create a generic JSON property bag and map the response into that. It's an interesting problem, I'm not sure how you're leveraging the fact of not knowing what will return in the response.
Posted by: Daniel | May 23, 2008 at 10:41 PM
I read this and I don't understand the specifics but I found the concept interesting and worthy of a Kick. Thanks
Posted by: Sam | May 26, 2008 at 02:04 AM
@Sam,
Thanks for your comment. As a riff I agree there's some leftover questions here but I'm working on a full treatment which I think will cover the specifics for a future post.
Posted by: Daniel | May 26, 2008 at 01:24 PM
Did you know that .NET has something built into it to uniquely identify types?
If you wanted to uniquely find an identifier for each "SocialEntity" type you had then you could use:
typeof(YourType).GUID
Note, I haven't looked into this too much but it does seem to be unique and not change across compiles.
Posted by: Ed Poore | May 30, 2008 at 02:04 PM