CODEDIGEST
Home » Articles
Search
 

Technologies
 

Sponsored links
 

CodeDigest Navigation
 

Technology News
No News Feeds available at this time.
 

Community News
No News Feeds available at this time.
 
ASP.NET Social Networks - Making Friends - Part 1

By DotNet Tutor
Posted On Jan 14,2009
Article Rating:
Be first to rate
this article.
No of Comments: 8
Category: ASP.Net
Print this article.

ASP.NET Social Networks - Making Friends - Part 1

Introduction

In this article by Andrew Siemer, we will cover the most important aspect of any community site—making friends. We will divide this article in two parts. This part starts with the discussion of Problem, that is, what we need to do to achieve success for the article's topic—finding and inviting friends to your network on a community site. It then moves to Design part where we decide on our requirements, and finally the article reaches Solution part where we begin discussing how to actually implement the features.

Problem

There are many aspects to building relationships in any community—real or virtual. First and foremost is initiating contact with the people whom you will eventually call your friends. We will do this in a few ways.

Ø       First, we will provide a way for our users to search the site for friends who are also members.

Ø       Second, we will create a form that allows you to enter your friends' email IDs and invite them directly.

Ø       Third, we will create a form that allows you to import all of your contacts from Outlook.

All of these methods of inviting a friend into the system would of course generate an email invite. The user would have the ability to then follow the link into the system and either sign up or log in to accept the request.

The preceding screenshot shows a sample email that the user would receive in their inbox. And following is the message that would be seen:

Once the user has clicked on the link in their email, he/she will be taken to a page displaying the request.

Once we have a way for our users to attach friends to their profile, we need to start integrating the concept of friends into the fabric of our site. We will need a way for our users to view all of their friends. We will also need a way for our users to remove the relationships (for those users who are no longer friends!).

Then we will need to add friends to our user's public profile.

While this is a good first pass at integrating the concept of friends into our site, there are a couple more steps for true integration. We need to add friend request and friend confirm alerts. We also need to modify the alert system so that when users modify their profile, change their avatar, or any other alert that is triggered by users of our system, all of their friends are notified on The Filter.

Once this is done we have one final topic to cover—which sort of fits in the realm of friends—the concept of Status Updates. This is a form of a micro blog. It allows users to post something about:

Ø       What they are currently doing

Ø       Where they are or

Ø       What they are thinking about

This is then added to their profile and sent out to their friends' filters.

The box in the preceding screenshot is where the user can enter his/herStatus Updates. Each of these updates will also be shown on the updates view and in their filter views.

This really helps to keep The Filter busy and helps people feel involved with their friends.

Design

Now let's talk about the design of these features.

Friends

This article is an attempt to throw light on the infrastructure needs and more heavily focused on the UI side for creating and managing relationships. That being said, there is always some form of groundwork that has to be in place prior to adding new features.

In this case we need to add the concept of a friend prior to having the ability to create friendships. This concept is a relatively simple one as it is really only defining a relationship between two accounts. We have the account that requested the relationship and the account that accepted the relationship.

This allows an account to be linked to as many other accounts as they wish.

Finding friends

Like in life, it is very difficult to create friends without first locating and meeting people. For that reason the various ways to locate and invite someone to be your friend is our first topic.

Searching for a friend

The easiest way to locate friends who might be interested in the same site that you are is to search through the existing user base. For that reason we will need to create a simple keyword search box that is accessible from any page on the site. This search feature should take a look at several fields of data pertaining to an account and return all possible users. From the search results page we should be able to initiate a friend request.

Inviting a friend

The next best thing to locating friends who are already members of the site is to invite people who you know out of the site. The quickest way to implement this is to allow a user to manually enter an email address or many email addresses, type a message, and then submit. This would be implemented with a simple form that generates a quick email to the recipient list. In the body of the email will be a link that allows the recipients to come in to our site.

Importing friends from external sources

An obvious extension of the last topic is to somehow automate the importing process of contacts from an email management tool. We will create a toolset that allows the user to export their contacts from Outlook and import them via a web form. The user should then be able to select the contacts that they want to invite.

Sending an invitation

With all the three of the above methods we will end up sending out an invitation email. We could simply send out an email with a link to the site. However, we need to maintain:

Ø       Who has been invited

Ø       Who initiated the invitation and

Ø       When this occurred

Then in the email, rather than just invite people in, we want to assign the user a key so that we can easily identify them on their way in. We will use a system-generated GUID to do this. In the case of inviting an existing user, we will allow him/her to log in to acknowledge the new friendship. In the case of a non-member user who was invited, we will allow him/her to create a new account. In both cases we will populate the invitation with the invited user's Account ID so that we have some history about the relationship.

Adding friend alerts to the filter

Once we have the framework in place for inviting and accepting friendship requests, we need to extend our existing system with alerts. These alerts should show up on existing user's Filters to show that they sent an invitation. We should also have alerts showing that a user has been invited. Once a user has accepted a friendship we should also have an alert.

Interacting with your friends

Now let's discuss some of the features that we need to interact with our friends.

Viewing your friends

Friends are only good if a user can interact with them. The first stop along this train of thought is to provide a page that allows a user to see all the friends he/she has. This is a jumping off point for a user to view the profile of friends. Also, as the concept of a user's profile grows, more data can be shown about each friend in an at-a-glance format. In addition to an all Friends page, we can add friends' views to a user's public profile so that other users can see the relationships.

Managing your friends

Now that we can see into all the relationships, we can finally provide the users with the ability to remove a relationship. In our initial pass this will be a permanent deletion of the relationship.

Following your friends

Now, we can extend the alert system so that when alerts are generated for a common user, such as updating their profile information, uploading a new photo, or any other user specific task, all the user's friends are automatically notified via their Filter.

Providing status updates to your friends

Somewhat related to friend-oriented relationships and The Filter is the concept of micro blogs. We need to add a way for a user to send a quick blurb about what they are doing, what they are thinking, and so on. This would also show up on the Filters of all the user's friends. This feature creates a lot of dynamic content on an end user's homepage, which keeps things interesting.




Solution

Now let's take a look at our solution.

Implementing the database

Let's look at the tables that are needed to support these new features.

The Friends Table

As the concept of friends is our base discussion for this article, we will immediately dive in and start creating the tables around this subject. As you have seen previously this is very straightforward table structure that simply links one account to the other.

Friend Invitations

This table is responsible for keeping track of who has been invited to the site, by whom, and when. It also holds the key (GUID) that is sent to the friends so that they can get back into the system under the appropriate invitation. Once a friend has accepted the relationship, their AccountID is stored here too, so that we can see how relationships were created in the past.

Status Updates

Status Updates allow a user to tell their friends what they are doing at that time. This is a micro blog so to speak.

A micro blog allows a user to write small blurbs about anything. Examples of this are Twitter and Yammer. For more information take a look here: http://en.wikipedia.org/wiki/Micro-blogging

The table needed for this is also simple. It tracks who said what, what was said, and when.

Creating the Relationships

Here are the relationships that we need for the tables we just discussed:

Ø       Friends and Accounts via the owning account

Ø       Friends and Accounts via the friends account

Ø       FriendInvitations and Accounts

Ø       StatusUpdates and Accounts

Setting up the data access layer

Let's extend the data access layer now to handle these new tables. Open your Fisharoo.dbml file and drag in these three new tables.

We are not allowing LINQ to manage these relationships for us. So go ahead and remove the relationships from the surrounding tables. Once you hit Save we should have three new classes to work with!

Building repositories

As always, with these new tables will come new repositories. The following repositories will be created:

Ø       FriendRepository

Ø       FriendInvitationRepository

Ø       StatusUpdateRepository

In addition to the creation of the above repositories, we will also need to modify the AccountRepository.

FriendRepository

Most of our repositories will always follow the same design. They provide a way to get at one record, many records by a parent ID, save a record, and delete a record. This repository differs slightly from the norm when it is time to retrieve a list of friends in that it has two sides of the relationship to look at—on one side where it is the owning Account of the Friend relationship and on the other side where the relationship is owned by another account. Here is that method:

public List<Friend> GetFriendsByAccountID(Int32 AccountID)
{
    List<Friend> result = new List<Friend>();
    using(FisharooDataContext dc = conn.GetContext())
    {
        //Get my friends direct relationship
        IEnumerable<Friend> friends = (from f in dc.Friends
                                       where f.AccountID == AccountID
                                       &&
                                       f.MyFriendsAccountID AccountID
                                       select f).Distinct();
        result = friends.ToList();
        //Getmy friends indirect relationship
        var friends2 = (from f in dc.Friends
                        where f.MyFriendsAccountID == AccountID &&
                        f.AccountID != AccountID
                        select new
                        {
                            FriendID = f.FriendID,
                            AccountID = f.MyFriendsAccountID,
                            MyFriendsAccountID = f.AccountID,
                            CreateDate = f.CreateDate,
                            Timestamp = f.Timestamp
                        }).Distinct();
        foreach (object o in friends2)
        {
            Friend friend = o as Friend;
            if(friend != null)
                result.Add(friend);
        }
    }
    return result;
}

This method queries for all friends that are owned by this account. It then queries for the reverse relationship where this account is owned by another account. Then it adds the second query to the first and returns that result. Here is the method that gets the Accounts of our Friends:

public List<Account> GetFriendsAccountsByAccountID(Int32 AccountID)
{
    List<Friend> friends = GetFriendsByAccountID(AccountID);
    List<int> accountIDs = new List<int>();
    foreach (Friend friend in friends)
    {
        accountIDs.Add(friend.MyFriendsAccountID);
    }
    List<Account> result = new List<Account>();
    using(FisharooDataContext dc = conn.GetContext())
    {
        IEnumerable<Account> accounts = from a in dc.Accounts
                                        where  
                                     accountIDs.Contains(a.AccountID)
                                        select a;
        result = accounts.ToList();
    }
    return result;
}

This method first gathers all the friends (via the first method we discussed) and then queries for all the related accounts. It then returns the result.

FriendInvitationRepository

Like the other repositories this one has the standard methods. In addition to those we also need to be able to retrieve an invitation by GUID or the invitation key that was sent to the friend.

public FriendInvitation GetFriendInvitationByGUID(Guid guid)
{
    FriendInvitation friendInvitation;
    using(FisharooDataContext dc = conn.GetContext())
    {
        friendInvitation = dc.FriendInvitations.Where(fi => fi.GUID
                                           == guid).FirstOrDefault();
    }
    return friendInvitation;
}

This is a very straightforward query matching the GUID values. In addition to the above method we will also need a way for invitations to be cleaned up. For this reason we will also have a method named CleanUpFriendInvitations().

public void CleanUpFriendInvitationsForThisEmail(FriendInvitation
                                                 friendInvitation)
{
    using (FisharooDataContext dc = conn.GetContext())
    {
        IEnumerable<FriendInvitation> friendInvitations = from fi in
                           dc.FriendInvitations
                           where fi.Email ==
                           friendInvitation.Email &&
                                                           
                           fi.BecameAccountID == 0 &&
                                                           
                           fi.AccountID == friendInvitation.AccountID
                           select fi;
        foreach (FriendInvitation invitation in friendInvitations)
        {
            dc.FriendInvitations.DeleteOnSubmit(invitation);
        }
        dc.SubmitChanges();
    }
}

This method is responsible for clearing out any invitations in the system that are sent from account A to account B and have not been activated (account B never did anything with the invite). Rather than checking if the invitation already exists when it is created, we will allow them to be created time and again (checking each invite during the import process of 500 contacts could really slow things down!). When account B finally accepts one of the invitations all of the others will be cleared. Also, in case account B never does anything with the invites, we will need a database process that periodically cleans out old invitations.

StatusUpdateRepository

Other than the norm, this repository has a method that gets topN StatusUpdates for use on the profile page.

public List<StatusUpdate> GetTopNStatusUpdatesByAccountID(Int32 AccountID, Int32 Number)
{
    List<StatusUpdate> result = new List<StatusUpdate>();
    using (FisharooDataContext dc = conn.GetContext())
    {
        IEnumerable<StatusUpdate> statusUpdates = (from su in
                                  dc.StatusUpdates
                                  where su.AccountID ==
                                  AccountID
                                  orderby su.CreateDate descending
                                  select
                                  su).Take(Number);
        result = statusUpdates.ToList();
    }
    return result;
}

This is done with a standard query with the addition of the Take() method, which translates into a TOP statement in the resulting SQL.

AccountRepository

With the addition of our search capabilities we will require a new method in our AccountRepository. This method will be the key for searching accounts.

public List<Account> SearchAccounts(string SearchText)
{
    List<Account> result = new List<Account>();
    using (FisharooDataContext dc = conn.GetContext())
    {
        IEnumerable<Account> accounts = from a in dc.Accounts
                where(a.FirstName + " " +
                      a.LastName).Contains(SearchText) ||
                    a.Email.Contains(SearchText) ||
                    a.Username.Contains(SearchText)
                select a;
        result = accounts.ToList();
    }
    return result;
}

This method currently searches through a user's first name, last name, email address, and username. This could of course be extended to their profile data and many other data points (all in good time!).

Implementing the services/application layer

Now that we have the repositories in place, we can begin to create the services that sit on top of those repositories. We will be creating the following services:

Ø       FriendService

In addition to that we will also be extending these services:

Ø       AlertService

Ø       PrivacyService

FriendService

The FriendService currently has a couple of duties. We will need it to tell us whether or not a user is a Friend, so that we can extend the PrivacyService to consider friends (recall that we currently only understand public and private settings!). In addition to that we need our FriendService to be able to handle creating Friends from a FriendInvitation.

public bool IsFriend(Account account, Account accountBeingViewed)
{
    if(account == null)
        return false;
    if(accountBeingViewed == null)
        return false;
    if(account.AccountID == accountBeingViewed.AccountID)
        return true;
    else
    {
        Friend friend =
                    _friendRepository.GetFriendsByAccountID
                    (accountBeingViewed.AccountID).
                    Where(f => f.MyFriendsAccountID ==
                    account.AccountID).FirstOrDefault();
        if(friend != null)
            return true;
    }
    return false;
}

This method needs to know who is making the request as well as who it is making the request about. It then verifies that both accounts are not null so that we can use them down the road and returns false if either of them are null. We then check to see if the user that is doing the viewing is the same user as is being viewed. If so we can safely return true. Then comes the fun part—currently we are using the GetFriendsByAccountID method found in the FriendRepository. We iterate through that list to see if our friend is there in the list or not. If we locate it, we return true. Otherwise the whole method has failed to locate a result and returns false.

Keep in mind that this way of doing things could quickly become a major performance issue. If you are checking security around several data points frequently in the same page, this is a large query and moves a lot of data around. If someone had 500 friends this would not be acceptable. As our goal is for people to have lots of friends, we generally would not want to follow this way. Your best bet then is to create a LINQ query in the FriendsRepository to handle this logic directly only returning true or false.

Now comes our CreateFriendFromFriendInvitation method, which as the name suggests, creates a friend from a friend invitation!

public void CreateFriendFromFriendInvitation(Guid InvitationKey, Account InvitationTo)
{
    //update friend invitation request
    FriendInvitation friendInvitation =
                    _friendInvitationRepository.
                    GetFriendInvitationByGUID(InvitationKey);
    friendInvitation.BecameAccountID = InvitationTo.AccountID;
    _friendInvitationRepository.SaveFriendInvitation(friendInvitation);
    _friendInvitationRepository.CleanUpFriendInvitationsForThisEmail(frie
                                                       ndInvitation);
    //create friendship
    Friend friend = new Friend();
    friend.AccountID = friendInvitation.AccountID;
    friend.MyFriendsAccountID = InvitationTo.AccountID;
    _friendRepository.SaveFriend(friend);
    Account InvitationFrom =
                           _accountRepository.GetAccountByID
                            (friendInvitation.AccountID);
    _alertService.AddFriendAddedAlert(InvitationFrom, InvitationTo);
    //TODO: MESSAGING - Add message to inbox regarding new
                       friendship!
}

This method expects the InvitationKey (in the form of a system generated GUID) and the Account that is wishing to create the relationship. It then gets the FriendInvitation and updates the BecameAccountID property of the new friend. We then make a call to flush any other friend invites between these two users. Once we have everything cleaned up, we add a new alert to the system letting the account that initiated this invitation know that the invitation was accepted.

AlertService

The alert service is essentially a wrapper to post an alert to the user's profile on The Filter. Go through the following methods. They do not need much explanation!

public void AddStatusUpdateAlert(StatusUpdate statusUpdate)
{
    alert = new Alert();
    alert.CreateDate = DateTime.Now;
    alert.AccountID = _userSession.CurrentUser.AccountID;
    alert.AlertTypeID = (int)AlertType.AlertTypes.StatusUpdate;
    alertMessage = "<div class="AlertHeader">" +
                 GetProfileImage(_userSession.CurrentUser.AccountID)
               + GetProfileUrl(_userSession.CurrentUser.Username) + "
             " + statusUpdate.Status + "</div>";
    alert.Message = alertMessage;
    SaveAlert(alert);
    SendAlertToFriends(alert);
}
public void AddFriendRequestAlert(Account FriendRequestFrom, Account
                   FriendRequestTo, Guid requestGuid, string Message)
{
    alert = new Alert();
    alert.CreateDate = DateTime.Now;
    alert.AccountID = FriendRequestTo.AccountID;
    alertMessage = "<div class="AlertHeader">" +
                   GetProfileImage(FriendRequestFrom.AccountID) +
                   GetProfileUrl(FriendRequestFrom.Username)
                   + " would like to be
                                             friends!</div>";
    alertMessage += "<div class="AlertRow">";
    alertMessage += FriendRequestFrom.FirstName + " " +
                    FriendRequestFrom.LastName +
                    " would like to be friends with you!  Click this
                     link to add this user as a friend: ";
    alertMessage += "<a href="" + _configuration.RootURL +
    "Friends/ConfirmFriendshipRequest.aspx?InvitationKey=" +
            requestGuid.ToString() + "">" + _configuration.RootURL +
    "Friends/ConfirmFriendshipRequest.aspx?InvitationKey=" +
            requestGuid.ToString() + "</a><HR>" + Message + "</div>";
    alert.Message = alertMessage;
    alert.AlertTypeID = (int) AlertType.AlertTypes.FriendRequest;
    SaveAlert(alert);
}
public void AddFriendAddedAlert(Account FriendRequestFrom, Account FriendRequestTo)
{
    alert = new Alert();
    alert.CreateDate = DateTime.Now;
    alert.AccountID = FriendRequestFrom.AccountID;
    alertMessage = "<div class="AlertHeader">" +
                  GetProfileImage(FriendRequestTo.AccountID) +
                  GetProfileUrl(FriendRequestTo.Username) + " is now
                  your friend!</div>";
    alertMessage += "<div class="AlertRow">" +
             GetSendMessageUrl(FriendRequestTo.AccountID) + "</div>";
    alert.Message = alertMessage;
    alert.AlertTypeID = (int)AlertType.AlertTypes.FriendAdded;
    SaveAlert(alert);
    alert = new Alert();
    alert.CreateDate = DateTime.Now;
    alert.AccountID = FriendRequestTo.AccountID;
    alertMessage = "<div class="AlertHeader">" +
                  GetProfileImage(FriendRequestFrom.AccountID) +
                  GetProfileUrl(FriendRequestFrom.Username) + " is
                  now your friend!</div>";
    alertMessage += "<div class="AlertRow">" +
           GetSendMessageUrl(FriendRequestFrom.AccountID) + "</div>";
    alert.Message = alertMessage;
    alert.AlertTypeID = (int)AlertType.AlertTypes.FriendAdded;
    SaveAlert(alert);
    alert = new Alert();
    alert.CreateDate = DateTime.Now;
    alert.AlertTypeID = (int) AlertType.AlertTypes.FriendAdded;
    alertMessage = "<div class="AlertHeader">" + GetProfileUrl(FriendRequestFrom.Username) + " and " +
                   GetProfileUrl(FriendRequestTo.Username) + " are
                   now friends!</div>";
    alert.Message = alertMessage;
    alert.AccountID = FriendRequestFrom.AccountID;
    SendAlertToFriends(alert);
    alert.AccountID = FriendRequestTo.AccountID;
    SendAlertToFriends(alert);
}

PrivacyService

Now that we have a method to check if two people are friends or not, we can finally extend our PrivacyService to account for friends. Up to this point we are only interrogating whether something is marked as private or public. Friends is marked false by default!

public bool ShouldShow(Int32 PrivacyFlagTypeID,
    Account AccountBeingViewed,
    Account Account,
    List<PrivacyFlag> Flags)
{
    bool result;
   
    bool isFriend = _friendService.IsFriend(Account,AccountBeingViewed);
    //flag marked as private test
    if(Flags.Where(f => f.PrivacyFlagTypeID == PrivacyFlagTypeID &&
                        f.VisibilityLevelID ==
                       (int)VisibilityLevel.VisibilityLevels.Private)
                       .FirstOrDefault() != null)
        result = false;
    //flag marked as friends only test
    else if (Flags.Where(f => f.PrivacyFlagTypeID ==
                         PrivacyFlagTypeID && f.VisibilityLevelID ==
                       (int)VisibilityLevel.VisibilityLevels.Friends)
                       .FirstOrDefault() != null && isFriend)
        result = true;
    else if (Flags.Where(f => f.PrivacyFlagTypeID ==
                         PrivacyFlagTypeID && f.VisibilityLevelID ==
                        (int)VisibilityLevel.VisibilityLevels.Public)
                        .FirstOrDefault() != null)
        result = true;
    else
        result = false;
    return result;
}

Summary

The article started with the thought process of how we can apply the concept of Friends to our community site. We tried to figure out what we need to do to implement the concept, we then finalized our requirements, and finally we began implementing the features. In the next part of this article we will continue with the implementation process.

This article is extracted from the book
ASP.NET 3.5 Social Networking
ASP.NET 3.5 Social Networking

Similar Articles