Architecting A C# Web-Based Application: Introduction

I am beginning a series of articles on architecting a “serious business” web-focused application. The raison d’être for this is because I have been unable to find a focused, well-documented sample project that exposes practical architecture and guidelines. The overall goal is to put myself on the line as a guinea pig, journal my thought process at every step, take the abuse and, hopefully, generate some positive discussion on the choices I make along the way. The secondary goal is to provide intermediate developers an example of how to approach a common type of project. As such, the series will not be an exploration of cutting-edge technologies, or of advanced coding techniques.

The sample application will be a project management application that follows the great majority of the basic Scrum principles, allows a development team to better manage their workload, and also maximizes the offload of data entry and organizing to the stakeholders as much as possible. I know that this type of application exists in umpteen forms on the net. Let’s face it, it’s basically the “enterprisey” equivalent of Tetris. However, I selected it for three main reasons:

  • The project is a neither too complex, nor uselessly simple in scope
  • It is a domain that should not be foreign to a readership composed of developers
  • I need a good app that fits the way my team works, rather than the overly generic and bloated PM software out there. While this may seem selfish, it’s actually good! I’ll be eating my own dogfood.

I think many developers don’t enjoy the use of most of the PM software out there because they become responsible for too much maintenance/clerical work. Not many packages out there put as much responsibility for the project in the hands of the stakeholders, and if they do, invariably they charge for more licenses. We’ll start simple, focus on providing a great UI, and won’t involve feature-creep just to gain bullet points on a sales presentation. Ultimately, the code base will be provided at large as an open-source project.

The whole solution will consist of your typical moving parts:

  • A web-based client where the majority of the interaction with the system is done, especially the collaborative parts.
  • A task bar application that allows quick data entry, primarily by developers, for common functions.
  • A middle layer built to handle the above two clients, and open for more. This will be where all the rules, workflows, transformations and other tasks happen. This will also force one to consider how to build the layers.
  • The database layer, obviously used to persist data.

The minimal functionality we will provide for a “v1.0” is:

  • The ability to work with the four general “things” found in Scrum: roles, timeboxes, artifacts and rules.
  • Allow business users to easily log ideas (a.k.a. user stories) and track the status thereof.
  • A robust and customizable workflow system for managing rules that, while not very dynamic, should be open to some amount of customization.
  • Some dashboarding for some simple metrics.
  • While we won’t aim to provide a full interactivity suite, it would be nice to build in a way to have a threaded discussion area for each idea or story, such that devs and users can collaborate and flesh out ideas in a way that doesn’t create an email nightmare.
  • A cross-platform, browser-based user interface with a form-based authentication system.
  • Optionally integrate logins with AD or other LDAP system.
  • A small taskbar application that helps you track what you are currently working on and gives feedback on changes in the app.

As you can see, this is not a simple throw-away project. But, neither is it a highly-complex, enterprise application. I hope that this series can help junior and intermediate .NET developers get a feel for how to approach the design of a web application, elevate my own game through feedback both high and low, and for intermediate to senior developers to collaborate on different approaches effectively by having the constraint of a defined scope in play. (Lots of times, comments on blogs like this run the gamut because some people are thinking of more complex projects than others.)

The next articles will cover the typical questions you (should?) have when you kick of a project:

  • What exactly am I talking about? Let’s spend some time on some diagramming and scoping to understand the larger moving parts. We’ll obviously iterate and refactor to get it right along the way, and so let’s not paralyze ourselves early, but we do need to establish some common language between the participants.
  • What tools am I going to use? For example, what framework will we use to get data in and out of our application? And, why? Another example is the many forms of IoC containers out there. Which one and why?
  • How am I going to structure all this practically? Let’s talk about overarching principles/methodologies that we will choose to apply on this project. Let’s also establish the subprojects in the overall solution from a technical standpoint.

Please, if you have any suggestions or comments, serve them up now! Especially if there’s anything in particular, top-level, that you think should be included. And, I hope you join me actively in subsequent posts.

Advertisements

MachineKey Key Generator

It’s not super-easy to find a versatile key generator for creating truly random keys to use in your MachineKey section in web.config or machine.config. So, here’s a quick console app, and related zipped project (Current extension is .doc to bypass WordPress’ file type limitations. Just save locally using “Save Link As..”, rename extension to .zip, unzip and enjoy.) to help you out with generating any of the various allowable keys you may need. Run it from inside a command-line window by calling:

MachineKeyGeneratorConsole.exe /v:<insert validation key type> /d:<insert decryption key type>

To save to a file, just type:

MachineKeyGeneratorConsole.exe /v:<insert validation key type> /d:<insert decryption key type> > key.txt

The switches are optional. Valid values for validation key generation are: AES, AES128, AES192, AES256, MD5, SHA1, HMACSHA1, SHA256, HMACSHA256, SHA384, HMACSHA384, SHA512, HMACSHA512. If no value is given, the default value is SHA1. Furthermore, although you can give different key sizes for some algorithms, the appropriate MachineKeyValidation is returned.

Valid values for decryption keys are: AES, AES128, AES192, AES256, DES, 3DES. If no value is given, the default value is AES.

using System;
using System.Diagnostics;
using System.Linq;
using System.Security;
using System.Security.Cryptography;
using System.Text;

namespace MachineKeyGeneratorConsole
{
    static class Program
    {
        static int Main(string[] args)
        {
            if (args.Any(arg =&gt; arg.ToLower().Equals("/t") || arg.ToLower().Equals("-t")))
                Trace.Listeners.Add(new ConsoleTraceListener(true));

            try
            {
                Run(args);
                return Environment.ExitCode;
            }
            catch (Exception e)
            {
                Console.Error.WriteLine(e.Message);
                Trace.TraceError(e.ToString());

                return (Environment.ExitCode != 0) ? Environment.ExitCode : 100;
            }
        }
        
        static void Run(string[] args)
        {
            int vkeybits = 256;
            string vkeyalg = "HMACSHA256";
            string vchoice = args.FirstOrDefault(arg =&gt; (arg.ToLower().Substring(0, 3).Equals("/v:") || arg.ToLower().Substring(0, 3).Equals("-v:")));
            if (vchoice != null)
            {
                Console.WriteLine("Requesting validation key: " + vchoice.Substring(3).ToUpper());
                switch (vchoice.Substring(3).ToUpper())
                {
                    case "AES128":
                        vkeybits = 128;
                        vkeyalg = "AES";
                        break;
                    case "AES":
                    case "AES192":
                        vkeybits = 192;
                        vkeyalg = "AES";
                        break;
                    case "AES256":
                        vkeybits = 256;
                        vkeyalg = "AES";
                        break;
                    case "MD5":
                        vkeybits = 128;
                        vkeyalg = "MD5";
                        break;
                    case "SHA1":
                    case "HMACSHA1":
                        vkeybits = 160;
                        vkeyalg = "SHA1";
                        break;
                    case "3DES":
                        vkeybits = 192;
                        vkeyalg = "3DES";
                        break;
                    case "SHA256":
                    case "HMACSHA256":
                        vkeybits = 256;
                        vkeyalg = "HMACSHA256";
                        break;
                    case "SHA384":
                    case "HMACSHA384":
                        vkeybits = 384;
                        vkeyalg = "HMACSHA384";
                        break;
                    case "SHA512":
                    case "HMACSHA512":
                        vkeybits = 512;
                        vkeyalg = "HMACSHA512";
                        break;
                    default:
                        vkeybits = 160;
                        vkeyalg = "SHA1";
                        break;
                }
            }

            int dkeybits = 192;
            string dkeyalg = "AES";
            string dchoice = args.FirstOrDefault(arg =&gt; (arg.ToLower().Substring(0, 3).Equals("/d:") || arg.ToLower().Substring(0, 3).Equals("-d:")));
            if (dchoice != null)
            {
                Console.WriteLine("Requesting decryption key: " + dchoice.Substring(3).ToUpper()); 
                switch (dchoice.Substring(3).ToUpper())
                {
                    case "AES128":
                        dkeybits = 128;
                        dkeyalg = "AES";
                        break;
                    case "AES":
                    case "AES192":
                        dkeybits = 192;
                        dkeyalg = "AES";
                        break;
                    case "AES256":
                        dkeybits = 256;
                        dkeyalg = "AES";
                        break;
                    case "3DES":
                        dkeybits = 192;
                        dkeyalg = "3DES";
                        break;
                    case "DES":
                        dkeybits = 64;
                        dkeyalg = "DES";
                        break;
                    default:
                        dkeybits = 192;
                        dkeyalg = "AES";
                        break;
                }
            }


            StringBuilder section = new StringBuilder();
            string vkey = GetRandomKey(vkeybits/8); // SHA
            string dkey = GetRandomKey(dkeybits/8); // AES can be 32, 48 or 64 chars, DES is 16 chars, 3DES is 48 chars
            section.AppendLine("");
            Console.WriteLine(section.ToString());

        }

        static string GetRandomKey(int bytelength)
        {
            int len = bytelength * 2;
            byte[] buff = new byte[bytelength];
            RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();

            rng.GetBytes(buff);

            StringBuilder sb = new StringBuilder(len);
            for (int i = 0; i &lt; buff.Length; i++)
                sb.Append(string.Format(&quot;{0:X2}&quot;, buff[i]));
            return sb.ToString();
        }
    }
}

The Dreaded Annual Review

I recently got pointed to an article in the New York Times on annual reviews. And, probably because my office AC isn’t functioning at top capacity, I got a little annoyed at yet another management article that forces another important personnel concept through some black-and-white filter and oversimplifies things.

Everyone will likely agree that that giving or receiving a well-received annual review is difficult. The article reinforces our fears by tying some horrible-sounding-and-dubiously-linked statistics on heart attacks, workplace stress, and by tossing around weighty organization names like the American Psychological Association, as if to drive home a point not with a hammer, but rather with a particle collider.

My current opinion is that there’s nothing wrong with having a good annual review. There’s obviously lots wrong with badly-formatted annual reviews. The trick to getting them right is to think about your approach to the reviews and how you treat them and the “secret sauce” is to frame them properly. Given that most people reading this blog are likely techies, let me rephrase it to say that the fundamental problem I’ve seen in annual reviews comes down to something akin to a “proper scoping” problem.

In IT, and more precisely in software development, scoping is a process whereby a “universe of discourse” is sized appropriately to the context. What is relevant to a small block of computer code, like a method on a class, is often not relevant to a larger scope, like the whole application. Not only is it not relevant, it should be hidden so as to not confound the whole system. But, sometimes, other pieces of information gets shared or pushed upwards from a small scope into a larger context and there are often guarded ways of doing this so as to keep things running smoothly.

The same concepts, I think, apply to performance reviews.

If you have a yearly review, hopefully not your only review process, your proper scope is the summary and trending from the year backwards through the year (or 1-3 years) forward. I treat it not like a micromanaged review of every day of the last year, whereby one nitpicks on the instances of failure.

career growth over time

Visualizing career-related growth

Instead, I imagine it like a projection of the data we (the reviewee and reviewer) have from the last year or so forward. Most managers fall into the trap of rehashing the one costly thing you did 11 months ago. That’s wrong, awful, and should have been taken care of on the spot, or at least through smaller-scoped meetings like daily huddles or weekly one-on-ones. By analogy, if you plot a line graph in your mind where the x-axis is time, and the y-axis is the effect on your career, and where each decision you made in the workplace either moved you up or down the y-axis by some subjective amount, I don’t want to talk about each stepwise movement one-by-one. What I want to cover, in a servant-leader-ish way, is the fact that it’s heading generally up, flat or down. Furthermore, what I really want to cover what we can do to affect that rate so that it is the most upward-looking rate the reviewee can achieve.

The annual review should be like a form of one-on-one strategic planning: strengths, weaknesses and long-term action plans that help the individual align themselves with the company and their team, and reach their goals. Done properly, they can be highly motivational. I use them, in a sense, to review each of my teammates’ BHAG, not the minutiae of individual actions I may or may not remember.

The annual review does not replace the smaller-scoped meetings, just like the daily huddle cannot serve the need for larger-scoped guidance (esp. in juniors). You have a collection of tools available to you as a manager. Why not use them?

Now, the common criticisms of annual reviews are:

  • Managers playing favorites: One common complaint is that pet employees never get dinged as badly as the less favored one. And, what’s worse, it’s not an extremely rare occurrence. However, I’d argue that this is not a reason to avoid annual reviews. Any such favoritism is/would be an issue in an annual, a quarterly, a weekly or any daily reviews. If the manager lacks the ability to bring as much objectivity as possible into this process and to work for the team and the company, then the fault lies with the manager, not the review.
  • One-Sidedness: Another common complaint is the one-sidedness of the review. The reviewee sits down and is forced to listen to how he or she fared, with limited viewpoints. Again, this is a problem if one approached the yearly as an “itemized recap”, or ” event-based debriefing”. Taking more of a trend-like, aggregated approach, mixed in with more forward-looking goal-seeking would negate this problem.
  • Compensation Tie-In: Simple. Don’t tie compensation (or termination of compensation) to these types of reviews that are more akin to brainstorming on 10K-foot career growth plans. Personally, I prefer to look at a) growth in responsibility and skills in operation in the team, and b) trends infrequently evaluated perfomance metrics.

It should not be “banished”. It should be done… right.

Silly C# Dictionary Error

So, I stumbled across an interesting error while coding today, which in hindsight is really simple (less tactful and more self-deprecating would be to say “stupid”). In C# (3.0), there is a set of classes that supply you with some really common generic data structures, such as the Stack or the Queue. Using generics, you specify the type(s) such that you can have handy, type-safe collections to work with.

In a small bit of code, I was making use of the Dictionary, more specifically Dictionary. I was first filling it with keys only, which corresponded to specific pages in a large PDF. I then iterated (foreach) through said Dictionary a second time, chopping up the large PDF based on the indexed pages, creating smaller ones, and putting the resultant file name in the value part of the KeyValuePair. Many times, we fill and iterate in a read-only way with the structures, so this error is uncommon if you forget something basic about them.

When first stepping through my code, on the second foreach block, I got a “Collection was modified; enumeration operation may not execute.” Wha? I had forgotten that you will get this error if any reference in the Dictionary gets changed to a new reference. IOW, if you take an instance of a class that is stored in the Dictionary and replace it with a different one, while looping, the IEnumerator notices the change and chokes on the “dirty” collection.

You are allowed to change the properties of objects stored in your collection, though. So, a simple fix is that instead of using a string, which is immutable, and doing something like:

Dictionary pageDictionary<int,string> = new Dictionary();
...
for (int i = 1; i <= theDoc.PageCount; i++)  // first loop
{
  pageDictionary.Add(theDoc.PageNumber, "")
}
...
foreach (KeyValuePair<int,string> idxPair in pageDictionary)  // second loop
{
  pageDictionary[pageIndex] = extractFileName;
}

You should use StringBuilder, which is like String, but mutable:

Dictionary pageDictionary<int,StringBuilder> = new Dictionary();
...
for (int i = 1; i <= theDoc.PageCount; i++)  // first loop
{
  pageDictionary.Add(theDoc.PageNumber, "")
}
...
foreach (KeyValuePair<int,StringBuilder> idxPair in pageDictionary)  // second loop
{
  pageDictionary[pageIndex].Append(extractFileName);
}

Duh. Silly me.

Adding A BitTorrent Client To A Proxmox Host

The Proxmox VE (a.k.a. PVE) host does not have a BitTorrent client by default, but this is a common way to download stuff in the Linux world. Here are the steps I took to setup BitTorrent on the PVE host system.

The PVE host environment is basically a Debian installation, so the first step is to download the newest BitTorrent tarball to a convenient directory. There are deb packages, but we will need to get a little manual to get it installed properly in a 64-bit OS. I SSH’ed into the host as root, created and used a /root/downloads/ directory for the download. Make sure to verify the BitTorrent site and download the newest version of the client.

The easiest way to download is to use wget:

On an unmodified PVE host, you’ll be missing a lot of dependent packages and should install those beforehand. These packages include python-wxgtk2.6, python-crypto, python-twisted and python-zopeinterface. Install each package with:

# apt-get install [package name].deb

There is one more dependency package that cannot be installed, python-psyco, as there is no 64-bit version and PVE is 64-bit. BitTorrent seems to work without it, though. The only way to deal with this is to edit the dependency out of the deb package and regenerate a deb package.

# tar xpfz BitTorrent-5.2.2.tar.gz
# cd BitTorrent-5.2.2
# vi debian/control

In the file, remove and locate the dependency on python-psyco by deleting the ‘psyco’ entry from the Depends line. This will drop the check when you install it. Next, I install sudo and use it to get BitTorrent rebuilt with the changed file that results in a new package.

# apt-get install sudo
# sudo bash install_nix.sh

Finally, install the BitTorrent client and download torrent files by calling bittorrent-console at the command line:

# cd dist
# dpkg -i bittorrent_5.2.2_python2.4.deb
# bittorrent-console ‘some-link-to-a-torrent-file’

By default, the file will end up in /root/BitTorrent Downloads/. Now, you can download to your heart’s content, pun intended…

Top 13 Funny Software Development Quotes

Over the years, I’ve collected some of the smartest-yet-funny software development quotes I have read. Here’s the current short list in no particular order. Oddly enough, there are thirteen of them and they all address the woes of programming.

Feel free to add any like quotes in the comment section!

  1. “The first 90% of the code accounts for the first 90% of the development time. The remaining 10% of the code accounts for the other 90% of the development time.” – Tom Cargill
  2. “In order to understand recursion, one must first understand recursion.” – Author Unknown
  3. “I have always wished for my computer to be as easy to use as my telephone; my wish has come true because I can no longer figure out how to use my telephone.” – Bjarne Stroustrup
  4. “A computer lets you make more mistakes faster than any other invention in human history, with the possible exceptions of handguns and tequila.” – Mitch Ratcliffe
  5. “There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult.” -C.A.R. Hoare
  6. “The gap between theory and practice is not as wide in theory as it is in practice.” – Author Unknown
  7. “If builders built buildings the way programmers wrote programs, then the first woodpecker that came along would destroy civilization.” – Gerald Weinberg
  8. “If debugging is the process of removing software bugs, then programming must be the process of putting them in.” – Edsger Dijkstra
  9. “Measuring programming progress by lines of code is like measuring aircraft building progress by weight.” – Bill Gates
  10. “Nine people can’t make a baby in a month.” – Fred Brooks
  11. “Programming today is a race between software engineers striving to build bigger and better idiot-proof programs, and the Universe trying to produce bigger and better idiots. So far, the Universe is winning.” – Rich Cook
  12. “There are two major products that come out of Berkeley: LSD and UNIX. We don’t believe this to be a coincidence.” – Jeremy S. Anderson
  13. “Before software can be reusable it first has to be usable.” – Ralph Johnson

Outsourced Or In-House Developers?

So, you have a workload of software projects, a veritable plethora of projects to get off your plate. Or, maybe you are considering an expansion of your capabilities. Your current team is tapped out… hopefully not overworked and tired; you don’t want to break their backs.

WARNING: I will be using the word “outsourcee” to refer to any entity not in-house performing work on behalf of your company. There’s nothing wrong with the word itself, although it’s full of negative connotations of lost jobs, cheap labor and evil executives. But, if you are a die-hard isolationist or a nationalist, you will be unable to read further. You have been warned. 🙂

You’re looking at various reasons and wondering if you should hire more in-house developers, or farm out. Here’s the top-level factors I look at to help me decide when to farm the work out. Of course, this is all from an SMB point-of-view; larger companies have a completely different perspective and set of tests to check against.

  • Niche/Esoteric Skills: If the project involves some fringe technology, or unique skill set that will not see much re-use in-house, then farm it out. Note that this could be an emerging technology, or an old one that would bore your in-house team to tears. Usually, hiring in-house for this is a big challenge anyways…
  • Time-To-Market Pressure: If the project involves the need to get something out before competitors claim the territory, farm it out. You should seek an outsourcing shop that will properly staff up so as to get your project out in the time frame you set for it. You’ll pay through the nose, but that may be the difference between sick or swim…
  • Overwhelming Workload: Obviously, one reason to consider farming out is if you have a large spike in the overall short-term workload. This is quite common in small businesses. Instead of slowing down and stretching thin, just bring in short-term, contract-based workers to shore up your team(s).
  • One-off or Short-Term Project: If the project is a side-bar to a core project, or some “throw-away” task where ongoing maintenance is not going to be an issue, consider farming it out and don’t bog down your core, in-house team(s) with it.
  • Experience Gain: You may want to consider randomly outsourcing the “easy, non-critical ones” just to gain experience with outsourcing options in your local area, or the broader market, simply so that you then know a little bit more about who to go to when it really counts.

There are undoubtedly more reasons, but I think these are the main, big ones. The more these reasons exist in any one project, the more you should consider farming out the work. Otherwise, don’t. It is better, and less costly, to go in-house. No really!

First, less face it, every dollar you spend creates/elevates talent in some individual, and it is generally best to keep that talent close to your vest rather than see it go to another company, especially a competitor.

Second, there’s more to developers than churning out code, even the so-called “boilerplate” stuff. Being able to effectively understand and translate requirements and interact with business users comes, in large part, from close familiarity with the people and the processes, a.k.a. the “ramp-up” or “learning curve”, and the short feedback cycle that comes from being in-house. These two have had a larger, empirical, hard-to-measure impact on any team’s performance than anything else. It doesn’t help the “super-awesome outsourced guru” if he has to wait two days for an email reply on a process issue from some department because they are swamped with some monthly peak of work. Every time you farm out, you’ll hit this over and over again. In-house is undoubtedly both a short-term and long-term investment. Treat it as such.

Lastly, contrary to the typical assumption I hear, farming out is usually not cheaper. A good, in-house team will simply cost less than an equivalent, good outsourced team, even if you consider all indirect costs. My off-the-cuff, empirical, “from-the-SMB” conversion factor is that outsourced is 2-4x more than in-house. One main problem is that because the outsourcing market, even post-dotcom bubble, has an incredibly high markup on IT work. Unfortunately, the perception that outsourcing is cheaper is because the comparison is usually between a bad, in-house team and a good outsourced consultancy.

If you are going to farm it out, the next question typically becomes who to farm out to. There are four general categories:

  • Freelancers: These are the “unincorporated” outsourcees that cost on the low-end of my conversion factor. They usually don’t have much overhead to worry about because the job isn’t their primary source of income. Lots of developers from large, status-quo-loving companies want to expand their skills and so pick up work on the side. Or, out-of-work, between-job developers will take on short-term contracts to fill the income gap. The main and dominant problem is the lack of accountability on multiple levels, from the personal involvement to the legal ramifications if they drop out on you. If you don’t know any freelancers, consider shopping on websites like Rent-A-Coder.
  • Local Outsourcee: The local outsourcee is a local, incorporated shop, usually with a smallish team or teams, that makes a living providing recurrent, happy IT service. They are usually not cheap, because of their business overhead. However, as a manager, if I can’t keep the capital in the house, I sure try to keep it in the neighborhood, because it eventually comes back to us. I’m not talking about “paying it forward” (although if that does happen, then great!), just that a vibrant local economy is indirectly good for my business. On a selfish level, it also helps me build out my local personal and corporate network of contacts.
  • Remote Outsourcee: There are all sorts of levels of outsourcing here, ranging from neighboring states, to neighboring countries to full use of sweatshops in Central Nepal. If you are an SMB, you should really not go to this option, unless you have a direct reference to an active shop used actively by another SMB you know and trust. I have heard too many horror stories of SMBs burned by this kind of outsourcing. The problem is that lots of these big offshore consultancies are driven by big customers and you’ll simply get lost in the mix. The smaller, satellite consultancies that emerge in such areas are usually unable to serve you properly because the big shops drain the area of real talent. The former is exorbitantly expensive, the latter is expensive in terms of direct cost due to the inevitable mess-ups. You’d be surprised how not cheap offshore work in places like India has become.
  • Internship: Don’t forget to consider using variants of the idea of internship. The concept is that you get some raw talent and let him/her beat his head on a project in exchange for a low wage and a great reference. Like farming out locally, there’s a lot of goodwill generated in this route. You may find your next hire in an intern; you certainly may find out who not to hire too. Each individual will hopefully go on to speak highly of you to other students or junior co-workers, generating potential future hiring opportunities. But, if you want to do this right, don’t hire just to stick a kid in the corner cube, and leave him or her to sink-or-swim. What you save in hard dollars will have to be spent in your own or senior developer’s hand-holding time.

This pretty much sums up how I consider outsourcing, short of a lengthier novella on the subject.

One last warning: Be open with the work and get your team’s opinion on farming out any work. Don’t let them hear about it indirectly and give them the opportunity to step up if they want and can deliver. Psychologically, from a morale point-of-view, keeping your team out of the loop is a big no-no.

What are your experiences? Do they match up with the above?