Searching for a user in AD in .NET 3.5

.NET 3.5 brought with it the new System.DirectoryServices.AccountManagement namespace, which makes working with Active Directory so much easier. Let's take a look at how to find a user if you know their username (SAMAccountName):

private static UserPrincipal GetPrincipal(string name)
{
    var context = new PrincipalContext(
        ContextType.Domain, "yourdomain.com");

    var principal = UserPrincipal.FindByIdentity(
        context, IdentityType.SamAccountName, name);

    return principal;
}

Wow, three entire lines of code. I don't know how we'll ever convince anyone to switch from the old way of doing things. :-) How about getting the user's groups?

PrincipalSearchResult<Principal> GetGroups(UserPrincipal principal)
{
    return principal.GetAuthorizationGroups();  
}

How about you just want to dump properties about that principal?

private static IDictionary<string, string> GetProperties(UserPrincipal principal)
{
    var properties = new Dictionary<string, string>();

    var directoryEntry = principal.GetUnderlyingObject() as DirectoryEntry;

    var allProperties = directoryEntry.Properties;

    foreach (var property in allProperties.PropertyNames)
    {
        var propertyName = property.ToString();

        var value = string.Empty;

        if (allProperties[propertyName] != null && allProperties[propertyName].Count > 0)
        {
            foreach (var val in allProperties[propertyName])
            {
                if (val != null)
                {
                    value += ", " + val.ToString();
                }
            }
        }

        properties.Add(property.ToString(), value); 
    }

    return properties;
}

Developing UCMA client in .NET 4.0

If you try to run a UCMA client application in .NET 4.0, you might get this exception when you try to create the CollaborationPlatform:

System.IO.FileLoadException was unhandled
    Message=Mixed mode assembly is built against version 'v2.0.50727' of the runtime and cannot be loaded in the 4.0 runtime without additional configuration information.
    Source=Microsoft.Rtc.Collaboration

Add the following lines to your configuration file under the root configuration element to make the error go away:

<startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0"/>
</startup>

Starting a conversation in UCMA

In a previous article, we learned how to get presence using UCMA. In this article, we'll learn how to start a conversation and send messages using UCMA.

The code for the entire program is located near the end of the article.

The process for starting a conversation is incredibly easy:

  1. Have the UserEndpoint create a Conversation
  2. Create a new InstantMessagingCall
  3. Establish the new IM call

The code:

public class ConnectionService 
{
    public ConversationCreateConversation()
    {
        return new Conversation(_endpoint);
    }
}

public class ConversationService
{
    private ConnectionService _connectionService;
    private Conversation _currentConversation;
    private InstantMessagingCall _currentIMCall;

    public ConversationService(ConnectionService connectionService)
    {
        _connectionService = connectionService;
    }

    public IAsyncResult StartConversationWith(string contactSipUri)
    {
        _currentConversation = _connectionService.CreateConversation();
        _currentIMCall = new InstantMessagingCall( _currentConversation);

        return _currentIMCall.BeginEstablish(
            contactSipUri,
            new ToastMessage("New conversation"), 
            new CallEstablishOptions(),
            EstablishCompleted, 
            null);
    }

    private void EstablishCompleted(IAsyncResult result)
    {
        _currentIMCall.EndEstablish(result);
    }
}

Ending a conversation is similarly simple:

public class ConversationService
{
    public IAsyncResult EndConversation()
    {
        return _currentConversation.BeginTerminate(TerminateCompleted, null);
    }

    private void TerminateCompleted(IAsyncResult result)
    {
        _currentConversation.EndTerminate(result);
    }
}

The remaining task is to send messages:

public class ConversationService
{
    public IAsyncResult SendMessage(string message)
    {
        return _currentIMCall.Flow.BeginSendMessage(message, SendMessageCompleted, null);
    }

    private void SendMessageCompleted(IAsyncResult result)
    {
        _currentIMCall.Flow.EndSendMessage(result);
    }
}

Here is the whole console application in one place:

using System;
using System.Net;
using Microsoft.Rtc.Signaling;
using Microsoft.Rtc.Collaboration;

namespace OCS_IM
{
    public static class Program
    {
        public static void Main(string[] args)
        {
            var connection = new ConnectionService();

            connection.Start().AsyncWaitHandle.WaitOne();

            connection.InitEndpoint(
                "sip:you@domain", 
                "ocs-server-name")
                .AsyncWaitHandle.WaitOne();

            var conversation = new ConversationService(connection);

            conversation.StartConversationWith("sip:contact@domain")
                .AsyncWaitHandle.WaitOne();

            conversation.SendMessage("A message");

            Console.WriteLine("Press any key to end program.");
            Console.ReadLine();

            conversation.EndConversation().AsyncWaitHandle.WaitOne();
            connection.Stop().AsyncWaitHandle.WaitOne();

            Console.ReadLine();
        }
    }

    public class ConversationService
    {
        private ConnectionService _connectionService;
        private Conversation _currentConversation;
        private InstantMessagingCall _currentIMCall;

        public ConversationService(ConnectionService connectionService)
        {
            _connectionService = connectionService;
        }

        public IAsyncResult StartConversationWith(string contactSipUri)
        {
            _currentConversation = _connectionService.CreateConversation();
            _currentIMCall = new InstantMessagingCall(_currentConversation);

            return _currentIMCall.BeginEstablish(
                contactSipUri,
                new ToastMessage("New conversation"),
                new CallEstablishOptions(),
                EstablishCompleted,
                null);
        }

        private void EstablishCompleted(IAsyncResult result)
        {
            _currentIMCall.EndEstablish(result);
        }

        public IAsyncResult SendMessage(string message)
        {
            return _currentIMCall.Flow.BeginSendMessage(
                message, SendMessageCompleted, null);
        }

        private void SendMessageCompleted(IAsyncResult result)
        {
            _currentIMCall.Flow.EndSendMessage(result);
        }

        public IAsyncResult EndConversation()
        {
            return _currentConversation.BeginTerminate(
                TerminateCompleted, null);
        }

        private void TerminateCompleted(IAsyncResult result)
        {
            _currentConversation.EndTerminate(result);
        }
    }

    public class ConnectionService
    {
        private CollaborationPlatform _platform;
        private UserEndpoint _endpoint;

        public IAsyncResult Start()
        {
            var settings = new ClientPlatformSettings("OCS_IM", SipTransportType.Tls);

            _platform = new CollaborationPlatform(settings);

            return _platform.BeginStartup(StartupCompleted, null);
        }

        private void StartupCompleted(IAsyncResult result)
        {
            _platform.EndStartup(result);
        }

        public IAsyncResult InitEndpoint(string currentUserSipUri, string ocsServerName)
        {
            var settings = new UserEndpointSettings(currentUserSipUri, ocsServerName);

            _endpoint = new UserEndpoint(_platform, settings);
            _endpoint.Credential = CredentialCache.DefaultNetworkCredentials;

            return _endpoint.BeginEstablish(EstablishCompleted, null);
        }

        private void EstablishCompleted(IAsyncResult result)
        {
            _endpoint.EndEstablish(result);
        }

        public Conversation CreateConversation()
        {
            return new Conversation(_endpoint);
        }

        public IAsyncResult Stop()
        {
            return _endpoint.BeginTerminate(TerminateCompleted, null);
        }

        private void TerminateCompleted(IAsyncResult result)
        {
            _endpoint.EndTerminate(result);
            _platform.BeginShutdown(ShutdownCompleted, null);
        }

        private void ShutdownCompleted(IAsyncResult result)
        {
            _platform.EndShutdown(result);
        }
    }
}

Change permissions of multiple files via Mac Terminal

If you want to make a file writable, it's no big deal. Open the "Info" window and update the file permissions at the bottom. However, this approach is painful if you need to change the permissions on multiple files. Enter Terminal:

chmod -R 755 name-of-the-directory-where-files-are-located

If you own the directory and the files, this will work. Otherwise, use:

sudo chmod -R 755 name-of-the-directory-where-files-are-located

The "-R" recursively sets permissions on all files and folders contained within the parent directory. The first number is the access given to the owner; the second number, to the file group's owner; and the third number, to everyone else. Here are the permissions:

  1. Read = 4
  2. Write = 2
  3. Execute = 1
  4. No access = 0

So if you want to give read, write, and execute access, the number you want is 4 + 2 + 1 = 7.

Creating charts in .NET

I think it's safe to say that most people prefer seeing summaries in charts instead of having to slog through rows and columns in an Excel spreadsheet. There are many frameworks out there to help you create charts in your .NET applications. Microsoft released a chart control at the end of 2008 that allows you to create many kind of charts1. Best of all, the chart control is completely free for you to use in any way you want.

Let's see how easy it is to create a chart.

Most Populated Cities In The World

Installations

Before you can start using the chart control, you need to install:

  1. Microsoft Chart Controls for Microsoft .NET 3.5
  2. Microsoft Chart Controls Add-on for Microsoft Visual Studio 20082

Microsoft also offers samples and documentation, which you can download at:

  1. Microsoft Chart Controls Samples
  2. Microsoft Chart Controls Documentation

Getting started

The first step is to setup the data points. You can do it programmatically; for example, you can (and often will) get the data from a database. For the sake of simplicity, we will define static data points in this article:

<asp:Chart ID="MostPopulatedCitiesChart" Height="300" Width="800" runat="server">
    <Series>
        <asp:Series 
            Name="Cities" 
            ChartType="Column" 
            ChartArea="MainChartArea" 
            YValueType="Int32" >
            <Points>
                <asp:DataPoint AxisLabel="Shanghai" YValues="7174" />
                <asp:DataPoint AxisLabel="Mumbai" YValues="22937" />
                <asp:DataPoint AxisLabel="Karachi" YValues="3683" />
                <asp:DataPoint AxisLabel="Delhi" YValues="29149" />
                <asp:DataPoint AxisLabel="Istanbul" YValues="6211" />
                <asp:DataPoint AxisLabel="São Paulo" YValues="7247" />
                <asp:DataPoint AxisLabel="Moscow" YValues="9722" />
                <asp:DataPoint AxisLabel="Seoul" YValues="17288" />
                <asp:DataPoint AxisLabel="Beijing" YValues="7400" />
                <asp:DataPoint AxisLabel="Mexico City" YValues="5954" />
            </Points>
        </asp:Series>
    </Series>
    <ChartAreas>
        <asp:ChartArea Name="MainChartArea" />
    </ChartAreas>
</asp:Chart>

That's all the "code" you need to write to show the chart:

Unformatted Chart

But it doesn't look very good. Let's make some formatting changes.

  1. Change the column colors and add gradients to something that's a little more pleasing.
  2. Add background gradients to the chart area.
  3. Lighten the gridlines.
  4. Add a title.
  5. Add labels to the axes.
  6. Show all column labels (via Interval="1")

What if you wanted to make it 3D? No big deal! Inside your chart area (""), add the following line:

<Area3DStyle Enable3D="true" />

And it's as simple as that!

3D Chart


  1. point, line, bar, doughnut, pie, circular, range, area, column, etc. 

  2. though it says 2008, it will work for 2010, as well. 

How to get presence via UCMA

UCMA is a client-side API that allows you to integrate OCS functionality into your application. You can retrieve presence, publish presence, manage your subscriptions, and start conversations and conferences.

In this article, we'll take a look at retrieving presence information from UCMA by subscribing to the contact.

SIP URIs

OCS identifies people through the SIP protocol. You can get a user's SIP address from Active Directory; each valid user will have a single, unique SIP address. SIP addresses generally are of the format:

sip:username@domain

Subscriptions vs. queries

Subscribing lets you get real-time presence updates per contact. So, if you are subscribed to John Smith, you will be notified any time his presence changes. Jane Smith’s presence, on the other hand, will become stale in your application because you’re not subscribed to her and you don’t get any notifications. In her case, you would have to ask OCS for her presence information every minute or ten minutes or whatever to get latest information.

Step 1: Initialize platform

Using UCMA starts with initializing the collaboration platform.

public class PresenceRetriever
{
    private CollaborationPlatform _platform;

    public IAsyncResult Start()
    {
        var settings = new ClientPlatformSettings("OCS_GetPresence", SipTransportType.Tls);

        _platform = new CollaborationPlatform(settings);

        return _platform.BeginStartup(InitCompleted, null);
    }

    private void InitCompleted(IAsyncResult result)
    {
        _platform.EndStartup(result);
    }
}

Step 2: Initialize endpoint

The next step is to initialize your application endpoint:

public class PresenceRetriever
{
    // ... 

    private UserEndpoint _endpoint;

    public IAsyncResult InitEndpoint(string currentUserSipUri, string ocsServerPath)
    {
        var settings = new UserEndpointSettings(currentUserSipUri, ocsServerPath);

        _endpoint = new UserEndpoint(_platform, settings);

        _endpoint.Credential = CredentialCache.DefaultNetworkCredentials;

        return _endpoint.BeginEstablish(EstablishCompleted, null);
    }

    private void EstablishCompleted(IAsyncResult result)
    {
        _endpoint.EndEstablish(result);
    }
}

Step 3: Subscribe to presence

You can subscribe to one or more presences at a time:

public class PresenceRetriever
{
    // ... 

    public void Subscribe(params string[] sipUris)
    {
        _endpoint.RemotePresence.PresenceSubscriptionCategories = 
            new string[] { "state" };

        _endpoint.RemotePresence.PresenceNotificationReceived += 
            PresenceNotificationReceived;

        var targets = new List<RemotePresentitySubscriptionTarget>();

        foreach(var sipUri in sipUris)
        {
            targets.Add(new RemotePresentitySubscriptionTarget(sipUri, null));
        }

        _endpoint.RemotePresence.BeginAddTargets(targets, AddTargetsCompleted, null);
    }

    private void PresenceNotificationReceived(object sender, RemotePresenceNotificationEventArgs e)
    {
        foreach(var notice in e.Notifications)
        {
            foreach(var category in notice.Categories)
            {
                var state = PresenceState.Create(category);

                Console.WriteLine(state.Availability);
            }
        }
    }

    private void AddTargetsCompleted(IAsyncResult result)
    {
        _endpoint.RemotePresence.EndAddTargets(result);
    }
}

If you run the application now, you should get notified every time the contacts you are subscribed to change their presences.

Final step: teardown

When your application is done with OCS, you need to teardown your endpoint and the platform. The code is simple:

public class PresenceRetriever
{
    // ... 

    public IAsyncResult Stop()
    {
        return _endpoint.BeginTerminate(TerminateCompleted, null);
    }

    private void TerminateCompleted(IAsyncResult result) 
    {
        _endpoint.EndTerminate(result);
        _platform.BeginShutdown(ShutdownCompleted, null);
    }

    private void ShutdownCompleted(IAsyncResult result)
    {
        _platform.EndShutdown(result);
    }
}

Asynchronous Calls

Notice how the startup and shutdown methods return IAsyncResults. The reason for this is that since the calls to OCS are performed asynchronously, you may get into situations where you try to establish the endpoint before the platform has been setup or you try to query for presence before the endpoint is established. The way to make sure that all the necessary steps are done is to use the result to figure out whether the operation has completed or force your calling method to wait until the operation has completed via AsyncWaitHandle.WaitOne method on your result.

Framework for creating flexible menus

Background

Navigation is a common component of almost every web application. You show a bunch of links somewhere on your site to allow your user to easily get from one section of the application to another. If your application is complicated, the business rules surrounding the navigation are likely also complicated. For example, only a regular user can see this link while an administrator can see that link while everyone can see some other link, and, oh, there is this one link that should show up if the current context satisfied some predicate.

I had to deal with complicated navigation in two different applications, recently, and I absolutely hated having to write a long chain of if-else statements. So I spent some time creating a framework that allows me to easily configure an application's menus and all the business rules surrounding the display, availability, and execution of each menu item.

The framework, dubbed Navisphere, is built for ASP .NET MVC in mind, but it can pretty easily be extended to regular .NET web applications, as well.

Features

There are three things about a menu item that might vary from user to user, all of which Navisphere handles:

  1. What to display? Example: "Request Something" to regular users vs. "Order Something" to admins.
  2. How to execute it? Example: "Home" goes to one page for regular users vs. another page for admins.
  3. Who can see it? Example: Admin link is available to admins and nobody else.

And, of course, the display, execution, and availability of a menu item can also be context-sensitive (i.e., dependent on something other than roles).

Usage

The idea is to be able to write something like the following on each page load to get a list of menu items that you have to show to the user:

var menu = menuCreator.CreateFor(myContext);

Setting up the menu

Our application's menu has three items with the following business rules:

Menu Item 1
    - Text = "Home" for everyone
    - Is available to everyone 
    - Goes to admin/index for administrators and home/index for regular users

Menu Item 2 
    - Text = "Edit" for everyone 
    - Is available to administrators 
    - Goes to admin/edit for everyone 

Menu Item 3
    - Text = "Manage User" for administrator and "Manage My Profile" for user
    - Is available to everyone 
    - Goes to user/edit for everyone 

Let's setup the menu with Navisphere:

    MenuCreator
        .Setup(item => item 
            .IsShownTo.Everyone.As("Home")
            .IsAvailableTo.Everyone
            .IsExecutedBy.Roles("Administrator").Via("index", "admin")
            .IsExecutedBy.Roles("Regular").Via("index", "home"))

        .Setup(item => item
            .IsShownTo.Everyone.As("Edit")
            .IsAvailableTo.Roles("Administrator")
            .IsExecutedBy.Everyone.Via("edit", "admin"))

        .Setup(item => item
            .IsShownTo.Roles("Administrator").As("Manage User")
            .IsShownTo.Roles("Regular").As("Manage My Profile")
            .IsAvailableTo.Everyone
            .IsExecutedBy.Everyone.Via("edit", "user"));

And, to display the menu:

<ul>
    <% foreach(var link in MenuBoostrapper.Menu) { %>
        <li>
            <%= Html.ActionLink(
                        link.DisplayText, 
                        link.Action, 
                        link.Controller, 
                        null, 
                        null) %>
        </li>
    <% } %> 
</ul>

Where MenuBootstrapper is:

public static class MenuBootstrapper
{
    private static readonly IMenuCreator<HttpContext> _menuCreator;

    public static IMenu Menu
    {
        get { return _menuCreator.CreateFor(HttpContext.Current); }
    }

    static MenuBootstrapper()
    {
        // Setup the _menuCreator here.
    }
}

Conclusion

Except in simple websites, navigation almost always ends up getting a little (or a lot) complicated. Instead of littering your code with a bunch of if or switch statements, a far better option is to create a small framework once that will allow you to setup your menus according to your business rules in a way that developers can understand and modify easily.

The framework will be available for download soon.

OpenID and ASP .NET MVC

OpenID is a centralized membership mechanism. You register at one site and use those credentials with all other sites that you want to be a member of. This means that you have only one username and password set to memorize and, more importantly, that password is stored at only one location. When you want to log into a site that is enabled with OpenID authentication, you're forwarded to your OpenID provider. When you login, you are redirected back to the original site with your unique authorization token.

There are a lot of OpenID providers out there, including Google, Yahoo!, and Flickr. If you don't want to use one of the available providers, you can always roll your own OpenID provider.

Setting up your website for OpenID authentication is typically super-easy.

OpenID frameworks for .NET

There are a couple of different OpenID frameworks for .NET, but the one with the most active development is DotNetOpenAuth. On top of providing OpenID support for both the 1.x and 2.0 versions, DotNetOpenAuth also implements OAuth, which is a protocol for secure authorization. DotNetOpenAuth supports OpenID development in ASP, classic ASP .NET web applications, as well as ASP .NET MVC websites.

In this article, we're going to talk about how to setup OpenID authentication in your ASP .NET MVC website. Our process will be as follows:

  1. When users want to login, we provide a way for the users to select the URL for their OpenID provider.
  2. We redirect the users to the provider for authentication.
  3. If login failed for whatever reason, we let the user know.
  4. Otherwise, we check to see if this is the first time user is logging into our site.
  5. If it is, we register the user and redirect the user to select his or her display name.
  6. We update last login date for the user -- after they selected their display name in case of registration.

Login ViewPage

Our login view page is fairly simple: it includes a textbox for the user to enter the OpenID provider url. There are quite a few javascript libraries out there that make this a one-click process. Check out ID Selector.

<h2>Login with OpenID</h2>

<% Html.BeginForm("Authenticate", "Login"); %>
    <fieldset title="Login via OpenID">
        <% Html.RenderPartial("ErrorControl"); %>
        <p>
            <label for="openIdIdentifier">
                Manually enter your OpenID URL: </label>
            <%= Html.TextBox("openIdIdentifier", null, new { @class = "openId" }) %>
            <input type="submit" value="Login" />
        </p>
    </fieldset>
<% Html.EndForm(); %>

Authenticate via OpenID

Once the Login button is clicked, we get taken to the Authenticate action, which looks like this:

public ActionResult Authenticate(string openIdIdentifier)
{
    var openId = new OpenIdRelyingParty();
    var response = openId.GetResponse();

    if (UserNeedsToLogin(response))
    {
        return AskUserToLogin(openIdIdentifier, openId);
    }

    return HandleAuthenticationResponse(response);
}

private bool UserNeedsToLogin(IAuthenticationResponse response)
{
    return response == null;
}

private ActionResult AskUserToLogin(string openIdIdentifier, OpenIdRelyingParty openId)
{
    var request = openId.CreateRequest(openIdIdentifier);

    return request.RedirectingResponse.AsActionResult();
}

private ActionResult HandleAuthenticationResponse(IAuthenticationResponse response)
{
    switch(response.Status)
    {
        case AuthenticationStatus.Authenticated:
            return HandleSuccessfulLogin(response);
        case AuthenticationStatus.Canceled:
            _context.ErrorMessage = "Login was cancelled at the provider.";
            break;
        case AuthenticationStatus.Failed:
            _context.ErrorMessage = "Login failed at the provider.";
            break;
        case AuthenticationStatus.SetupRequired:
            _context.ErrorMessage = "The provider requires setup.";
            break;
        default:
            _context.ErrorMessage = "Login failed.";
            break;
    }

    return View("Index");
}

Successful login

The code for successful login looks as expected given our requirements:

private ActionResult HandleSuccessfulLogin(IAuthenticationResponse response)
{
    var claimedIdentifier = response.ClaimedIdentifier;

    registerIfUserIsNotRegisteredYet(claimedIdentifier);
    currentUserIs(claimedIdentifier);

    if (_context.CurrentUser.HasYetToLogin)
    {
        return RedirectToAction("FirstTimeLogin");
    }

    currentUserIsLoggedIn();

    return View();
}

private void currentUserIsLoggedIn()
{
    _accountRepository.Login(_context.CurrentUser);
}

If it's the first time the user is logging in, we register the user with the identifier returned by the OpenID provider.

private void registerIfUserIsNotRegisteredYet(Identifier claimedIdentifier)
{
    if (_accountRepository.Fetch(claimedIdentifier) == null)
    {
        var regularRole = new string[] { "Regular" };

        var userToRegister = new Account(claimedIdentifier, regularRole)
        {
            Id = 0, 
            LastLoginAt = DateTime.MinValue, 
            Profile = new Profile { DisplayName = claimedIdentifier }
        };

        _accountRepository.Insert(userToRegister);
    }
}

Account implements IPrincipal, and Profile has only one property for now (DisplayName). (You can add any other properties that make sense to your application, like FirstName, LastName, Email, etc.)

Setting up the current user

We also need to let our application know who the current user is. I do that by setting a "CurrentUser" property on my "context" object:

private void currentUserIs(Identifier claimedIdentifier)
{
    var registeredUser = _accountRepository.Fetch(claimedIdentifier);

    _contextUser.CurrentUser = registeredUser;
}

If user is logging in for the first time, ask for required information

Normally, I use the username for both Identity.Name and DisplayName. But with OpenID, the identifiers can be long, bizarre, and useless, so we need the user to provide us a display name. When the user provides us with the display name, we update our repository with the information. Since we figure out whether or not the user has logged in before based on the last login date, we don't set it for first-time users until after the user has provided all the required information and we've updated our repository with the information. For all other users, we set the last login date as soon as the user has logged in.

[HttpGet]
public ActionResult FirstTimeLogin()
{
    return View();
}

[HttpPost]
public ActionResult FirstTimeLogin(string displayName)
{
    if (string.IsNullOrWhiteSpace(displayName))
    {
        ModelState.AddModelError("displayName", "Display name is required.");

        return View();
    }

    var userName = _context.CurrentUser.Identity.Name;

    _accountRepository.UpdateDisplayName(userName, displayName);

    currentUserIsLoggedIn();

    return RedirectToAction("Index", "Home");
}

Conclusion

Integrating OpenID with your ASP.NET MVC website feels more complicated than it really is. While we had to write a lot of code, none of it is particularly complicated or unintuitive. DotNetOpenAuth and ASP.NET MVC make it especially easy.

2 of 19 pages  <  1 2 3 4 >  Last »

On the Side