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; }
|