Extending community entities

by: Joel Abrahamsson

There was recently a thread in the EPiServer World forum about how to enable users to comment on a poll. I suggested a solution using EPiServer Communitys (actually EPiServer.Commons) attribute system to add a blog, where each entry would be a comment, to all polls . This got me thinking about different ways to extend community entities with other entities. In this article I’ll present three different ways of doing that. Two extend all entity types while one deals with extending a specific type.

Below I’ll describe ways to extend all entities that implement the IAttributeExtendableEntity interface with a blog that can be used for comments. The exact same approaches could however be used for extending entities with a ImageGallery, a VideoGallery, a Poll or any other Community entity.

Using a Extension Method

The below example will add a method named GetCommentsBlog to all instances of classes that implement the IAttributeExtendableEntity interface. As almost all entities in the Community Framework inherit from FrameworkEntityBase which in turns implements IAttributeExtendableEntity the method will be added to almost every single entity type in the community platform.

private const string COMMENTS_BLOG_ATTRIBUTE_NAME = "CommentsBlog";
 
public static Blog GetCommentsBlog(this 
    IAttributeExtendableEntity entity)
{
    //Ensure that the entitys type has an comments blog attribute
    IAttribute commentsBlogAttribute = 
        AttributeHandler.GetAttributes(entity).FirstOrDefault(
            attribute => 
                attribute.Name == COMMENTS_BLOG_ATTRIBUTE_NAME
        );
    if (commentsBlogAttribute == null)
    {
        commentsBlogAttribute = new Attribute(
            COMMENTS_BLOG_ATTRIBUTE_NAME, 
            entity.GetType(), typeof(Blog), false);
        AttributeHandler.AddAttribute(commentsBlogAttribute);
    }
 
    //Get the entitys comments blog, create a new one if none exists
    Blog commentsBlog = 
        entity.GetAttributeValue<Blog>("CommentsBlog");
    if (commentsBlog == null)
    {
        string name = 
            string.Format("Comments blog for {0} with ID {1}", 
                entity.GetType().Name, entity.ID);
        commentsBlog = new Blog(name);
        commentsBlog = BlogHandler.AddBlog(commentsBlog);
        entity = (IAttributeExtendableEntity)entity.Clone();
        entity.SetAttributeValue(COMMENTS_BLOG_ATTRIBUTE_NAME, 
            commentsBlog);
        Common.Data.FrameworkFactoryBase.UpdateEntity(entity);
    }
 
    return commentsBlog;
}

The method does three things. It first ensures that there is an attribute named “CommentsBlog” for the entity's type. It then ensures that that attribute has a value (a blog). Finally it returns the value.

An example of how to use the method:

public static EntryCollection GetLatestCommentsForPoll(Poll poll)
{
    EntrySortOrder latestSortOrder = new EntrySortOrder(
        EntrySortField.Created, SortingDirection.Descending);
    EntrySortOrder[] sortOrders = new [] { latestSortOrder };
 
    return poll.GetCommentsBlog().GetEntries(1, 10, sortOrders);
}

Using a Extension Method and a HTTP Module

While it’s pretty cool that we are able to extend all community entities with other entities using only a single extension method it’s not the best solution performance vice as we have to ensure that the attribute exists on each method call. Wouldn’t it be better if we only checked that the attribute exists once, when the application starts up? We can do that using a HTTP module.

Another problem with the above approach is also that we create a method that, when no blog exists, will update the instance it is invoked on without the invoker knowing that the entity is updated. So, let’s also move the creation of the blog from the extension method to the HTTP module.

public class CommentsBlogModule : IHttpModule
{
    private static bool _communityStarted;
    private static readonly Object _lockObject = new Object();
    internal const string COMMENTS_BLOG_ATTRIBUTE_NAME = 
        "CommentsBlog";
 
    public void Init(HttpApplication context)
    {
        context.EndRequest += EndRequest;
    }
 
    private static void EndRequest(object sender, EventArgs e)
    {
        if (_communityStarted)
            return;
 
        lock (_lockObject)
        {
            if (_communityStarted)
                return;
 
            EnsureAllEntitiesHaveAttribute();
 
            FrameworkFactoryBase.EntityAdded += 
                FrameworkFactoryBase_EntityAdded;
 
            _communityStarted = true;
        }
    }
 
    private static void EnsureAllEntitiesHaveAttribute()
    {
        Assembly[] assemblies = 
            AppDomain.CurrentDomain.GetAssemblies();
        foreach(Assembly assembly in assemblies)
        {
            EnsureEntityTypesInAssemblyHasAttribute(assembly);
        }
    }
 
    private static void EnsureEntityTypesInAssemblyHasAttribute(
        Assembly assembly)
    {
        Type[] types = assembly.GetTypes();
 
        foreach (Type type in types)
        {
            if(type.IsAbstract)
                continue;
 
            Type attributeExtendableType = 
                typeof (IAttributeExtendableEntity);
            if(attributeExtendableType.IsAssignableFrom(type))
                EnsureTypeHasAttribute(type);
        }
    }
 
    private static void EnsureTypeHasAttribute(Type type)
    {
        IAttribute commentsBlogAttribute = 
            AttributeHandler.GetAttributes(type).FirstOrDefault(
                attribute => attribute.Name == 
                    COMMENTS_BLOG_ATTRIBUTE_NAME);
 
        if (commentsBlogAttribute == null)
        {
            commentsBlogAttribute = 
                new Attribute(COMMENTS_BLOG_ATTRIBUTE_NAME, 
                type, typeof(Blog), false);
            AttributeHandler.AddAttribute(commentsBlogAttribute);
        }
    }
 
    static void FrameworkFactoryBase_EntityAdded(
        IEntityEventArgs args)
    {
        Type extendableType = typeof (IAttributeExtendableEntity);
        Type addedEntityType = args.Entity.GetType();
        if (!extendableType.IsAssignableFrom(addedEntityType))
            return;
 
        IAttributeExtendableEntity entity = 
            (IAttributeExtendableEntity)args.Entity;
 
        string name = string.Format(
            "Comments blog for {0} with ID {1}", 
            entity.GetType().Name, entity.ID);
        Blog commentsBlog = new Blog(name);
        commentsBlog = BlogHandler.AddBlog(commentsBlog);
 
        entity = (IAttributeExtendableEntity)entity.Clone();
        entity.SetAttributeValue(COMMENTS_BLOG_ATTRIBUTE_NAME, 
            commentsBlog);
        FrameworkFactoryBase.UpdateEntity(entity);
    }
 
    public void Dispose() {}
}

Add the module to the htppModules node in web.config if you are using IIS 5.1/6 or the modules node if you are using IIS 7. An example for a site running on IIS 5.1/6:

<httpModules>
  <add name="CommentsBlogModule" type="MyNamespace.HttpModules.CommentsBlogModule, MyNamespace" />
</httpModules>

The module does two things when the application is first started. It locates all types that implement the IAttributeExtendable interface using reflection and creates the “CommentsBlog” attribute for them if it doesn’t already exists.

It also adds an event handler to the EntityAdded event of the FrameworkFactoryBase class. When the event is fired the event handler will create a new blog and add it to the newly created entity, given that the entity’s type implements IAttributeExtendableEntity.

Our extension method for getting the CommentsBlog can now be drastically shortened:

public static Blog GetCommentsBlog(this 
    IAttributeExtendableEntity entity)
{
    return entity.GetAttributeValue<Blog>(
        HttpModules.CommentsBlogModule.COMMENTS_BLOG_ATTRIBUTE_NAME);
}

Extending a single type

Being able to extend all types is great but usually we only need to extend a single type. To do so we’ll start off by adding the “CommentsBlog” attribute to the Poll type. As we know exactly which type to add the attribute to we don’t have to do it programmatically but can instead do it in the admin interface.

Click on “Attributes” in the Community admin tab and then on the “Create Attribute” button and fill in the form like this:

image

We still need to create a blog for each new poll. Just as before this could be done in the extension method, but a cleaner approach is to do it just after the poll has been created using a HTTP module:¨

public class CommentsBlogModule : IHttpModule
{
    private static bool _communityStarted;
    private static readonly Object _lockObject = new Object();
    internal const string COMMENTS_BLOG_ATTRIBUTE_NAME = 
        "CommentsBlog";
 
    public void Init(HttpApplication context)
    {
        context.EndRequest += EndRequest;
    }
 
    private static void EndRequest(object sender, EventArgs e)
    {
        if (_communityStarted)
            return;
 
        lock (_lockObject)
        {
            if (_communityStarted)
                return;
 
            PollHandler.PollAdded += PollHandler_PollAdded;
 
            _communityStarted = true;
        }
    }
 
    static void PollHandler_PollAdded(string sender, 
        EPiServerCommonEventArgs e)
    {
        Poll poll = (Poll) e.Object;
 
        string name = string.Format(
            "Comments blog for the poll with ID {0}", poll.ID);
        Blog commentsBlog = new Blog(name);
        commentsBlog = BlogHandler.AddBlog(commentsBlog);
 
        poll = (Poll)poll.Clone();
        poll.SetAttributeValue(COMMENTS_BLOG_ATTRIBUTE_NAME, 
            commentsBlog);
        PollHandler.UpdatePoll(poll);
    }
 
    public void Dispose()
    {
    }
}

Finally, our extension method:

public static Blog GetCommentsBlog(this Poll poll)
{
    return poll.GetAttributeValue<Blog>(
        HttpModules.CommentsBlogModule.COMMENTS_BLOG_ATTRIBUTE_NAME);
}

Conclusion

These are a few of the many possible ways to extend community entities. While the last is probably the most useful in everyday scenarios I think the first two shows how powerful the entity provider pattern in the community platform is.

The full source code for the classes above can be downloaded here.

12 March 2009

Tags:


    Comments

    1. Hi Joel, welcome to the blog team! Nice post! rgds, Mike
    2. Great post!
    3. Excellent post, Joel! I'm sure these examples will be useful to other developers, both as-is and the general idea of how one can combine the different entities in EPiServer Community to add functionality where you need it in your particular project.
    4. Thanks for the excellent post! But why is the example url broken?
    Post a comment    
    User verification Image for user verification  
    EPiTrace logger