Build your conventions

Con­tin­u­ing the theme of con­ven­tions in code; I talked about val­i­dat­ing the con­ven­tions, but I didn't touch upon one more basic issue. What to base the con­ven­tions on? Short answer is — any­thing you like (as long as it’s black). Long answer is, well — longer.

Strong typed nature of .NET gives us rich set of infor­ma­tion we could use to build con­ven­tions on. Let's go over some of them. I will con­cen­trate mostly on sin­gle sce­nario as an exam­ple — IoC con­tainer reg­is­tra­tion, but the dis­cus­sion can be eas­ily extrap­o­lated to cover any other scenario.

Loca­tion

Assem­blies are the main, most coarse grained build­ing blocks of appli­ca­tion. While they carry quite a bit of infor­ma­tion we could use (name, ver­sion, strong name, cul­ture) it rarely makes sense to use all of that infor­ma­tion. Most com­mon usage is directly point­ing to our assem­bly of inter­est, either by name

GetAssemblyNamed("Acme.Crm.Services");

or in a more strongly typed man­ner for exam­ple via type con­tainer in that assem­bly in cases where we don't mind hav­ing strong ref­er­ence to that assem­bly in your com­po­si­tion root.

GetAssemblyContaining<Acme.Crm.EmailSenderService>();

Most of the time that is enough (at least at this level, in this situation).

Directly point­ing at cer­tain assem­bly is gen­er­ally what we want, and this is the most straight­for­ward and safe way to do that. In some cases (for exam­ple exten­si­bil­ity sce­nar­ios) you may want to leave the door open for con­sum­ing more than one assem­bly, often unknown at the time of com­pi­la­tion. In this case it may make sense to use as much infor­ma­tion as pos­si­ble to fil­ter out unwanted assem­blies as early as pos­si­ble. This obvi­ously depends on the specifics of the sce­nario at hand, for exam­ple who will be author­ing the exten­sions? If only you (mean­ing your team/company), you can use infor­ma­tion in assem­bly name and addi­tion­ally ver­ify the key that the assem­bly is signed with appro­pri­ate key.

GetAssembliesWithNameStartingWith("Acme.Crm.Extensions.")

   .GetAssembliesSingledWithTheSameKeyAs("Acme.Crm.Services");

In case where third party ven­dors will pro­vide exten­sions, you may just require that the assem­bly con­forms to cer­tain nam­ing pat­tern (name con­tains ".Crm.Extensions."). This not only gives you quick way of fil­ter­ing out assem­blies you're not inter­ested in — it also helps you keep your project cleaner. With con­ven­tion like this just a quick glance at direc­tory with your project's assem­blies will be enough infor­ma­tion to tell how many exten­sion assem­blies there are.

This obvi­ously is rarely enough — find­ing right assem­blies is often just the first step and there's a wealth of infor­ma­tion else­where you can use. Mod­ules are sel­dom use­ful as most of the time you have just one per assem­bly, so I'm men­tion­ing them only for com­plete­ness. Within (and between) assem­blies name­spaces are often used to par­ti­tion types. They have two impor­tant char­ac­ter­is­tics that can be use­ful when defin­ing con­ven­tions — their name, and hierarchy.

You rarely use name­spaces on their own. Most often they are one of a few bound­aries you set to nar­row down the set of types you're inter­ested in. For exam­ple you may find your repos­i­to­ries like this:

GetAssemblyContaining<CustomerRepository>()

   .GetTypesInNamespaceOf<CustomerRepository>()

   .SomeOtherConstraints()....

Broadly speak­ing you can find name­spaces using sim­i­lar tech­niques as with assem­blies — strongly typed, as demon­strated in this para­graph or via string.

GetTypesInNamespace("Acme.Crm.Repositories");

While the for­mer is more type safe and refac­tor­ing friendly, the lat­ter is more explicit and reads bet­ter — you get the infor­ma­tion you want directly, with­out hav­ing to go through addi­tional layer of indirection.

It is often valu­able to take advan­tage of name­space hier­ar­chy to use addi­tional layer in the hier­ar­chy to carry addi­tional infor­ma­tion. One such case could be to divide ser­vices in "Acme.Crm.Services" name­space to sub-namespaces based on their lifestyle, such that sin­gle­ton Cache­P­rovider­Ser­vice will live in "Acme.Crm.Services.Global" name­space, and per web request UnitOf­Work­Ser­vice would live in "Acme.Crm.Services.PerRequest".

Type

Types them­selves carry a lot of infor­ma­tion we can use for con­ven­tions: they have mean­ing­ful names (hope­fully), inherit other types, imple­ment inter­faces, have cus­tom attrib­utes, generic con­straints etc. There are two pop­u­lar usages for type names when deal­ing with conventions.

We often put mean­ing­ful suf­fixes, or more broadly speak­ing — name our types in a cer­tain, mean­ing­ful way. For exam­ple type imple­ment­ing IRepository<Customer> would be named Cus­tomer­Repos­i­tory. Or we may require all our ser­vices to have "Ser­vice" suf­fix. While the for­mer helps us keep thins clean and con­sis­tent the lat­ter (by some regarded as cer­tain form of dreaded Hun­gar­ian nota­tion), often serves as safety net layer for val­i­da­tion pur­poses, to help you catch cases where you by mis­take put a type in inap­pro­pri­ate namespace.

The other com­mon usage is using cer­tain pre­fixes to build dec­o­ra­tor chains. You may have the fol­low­ing types: Log­ging­Cus­tomer­Repos­i­tory, Caching­Cus­tomer­Repos­i­tory, Cus­tomer­Repos­i­tory, Log­ging­Pro­duc­tRepos­i­tory, Caching­Pro­duc­tRepos­i­tory, Pro­duc­tRepos­i­tory etc. When putting con­sis­tent, well known pre­fixes on your types you can use that infor­ma­tion to build dec­o­ra­tor chains, so that Logging*foo* will dec­o­rate Caching*foo* which will dec­o­rate actual *foo* type.

In IoC reg­is­tra­tion case base types (or inter­faces) are very com­monly used. Most pop­u­lar case if layer supertype.

RegisterTypesInheriting<IController>();

reg­is­ters all con­trollers in our application.

RegisterClosedVersionsOf(typeof(IRepository<>));

reg­is­ters all generic repos­i­to­ries we might have. It some­times may also be worth­wile to take greater advan­tage of generics.

Hav­ing interfaces:

public interface IHandler<TCommand> where TCommand: ICommand

{

   // ...something

}

 

public interface ICommand

{

   // ...something

}

we might quite eas­ily tie the two together — han­dlers to their respec­tive commands.

Attrib­utes, marker interfaces

In early days of IoC con­tain­ers it was com­mon to see cus­tom Attrib­utes being used for con­fig­ur­ing com­po­nents. In Java (which gained attribute-like abil­i­ties much later after .NET) con­tain­ers tend to pre­fer this approach even now. While this approach is gen­er­ally dis­cour­aged, using domain spe­cific attrib­utes, or mark­ing inter­faces for your com­po­nents may some­times be beneficial.

You may for exam­ple use attrib­utes to mark com­po­nents you want to dec­o­rate, (put [Cache­dAt­tribute] on repos­i­to­ries you want to put cache around). Mark­ing inter­faces can be use­ful for this as well — attrib­utes are pre­ferred solu­tion where you want to asso­ciate some addi­tional data with the com­po­nent (for exam­ple cache duration).

Sim­i­lar infor­ma­tion as out­lined in case of types, can be used to build con­ven­tions around type mem­bers. While it usu­ally makes lit­tle sense in con­text of IoC to do so, it's very use­ful in other cases — like Rob's MVVM frame­work and wiring Can*Foo* with *Foo* property/method pairs.

Wrap­ping up

Any­thing can be used as basis for con­ven­tion. You only have to know what makes sense in given con­text and looks for oppor­tu­ni­ties to improve that. There are a few things to keep in mind when decid­ing upon con­ven­tions. Keep them tight — use more than one con­straint in order to min­i­mize false pos­i­tives. For exam­ple use name­space, base type and name suf­fix to find repos­i­to­ries in your appli­ca­tion. You will pay small addi­tional price of more reflec­tion you do when start­ing, but it will save you lots of time try­ing to fig­ure out why your appli­ca­tion is mis­be­hav­ing later. Make your con­ven­tions well known to the entire team. Put short descrip­tion on a board in your team room, in wiki engine you're using, on in conventions.txt file in your solu­tion. In addi­tion to that make that rules exe­cutable to spot poten­tial mis­takes and dis­con­form­ing code (see my pre­vi­ous post for more detailed dis­cus­sion on the topic). Don't be afraid to refine the con­ven­tions. Once they are decided upon, they don't get writ­ten in stone. If your appli­ca­tion grows and you find them inad­e­quate, adjust them (try­ing not to loosen them up too much).