Is there a known best way to handle defaults?

Feb 12, 2009 at 10:29 PM
I have a task, whatever task, but I'm working with a naming service.

I wish to provide a default implementation. However, I wish to have other coders in the composable system able to create a naming service to replace mine. I am not sure the best approach to a couple of issues:

For context, people will want to implement only parts of the interface (I suspect a few things will be changed for style, and the whole shebang replaced for other cultures)

 - Provide a base class for them to derive from if they desire. 
        -In this case, can the base class implement the interface so there's less work to do? Or does the implementing interface have to be on the same class? Can the Export attribute be on the base class?
        -Is this too intimate a dependency from a philisophical point of view?
        -Does the very question imply that I have an insufficiently granular interface?    
- Provide an interface that is not for a class that does work, but a class that is called once to return delegates. 
        - This sounds easy for me, but doesn't sound like an approach for mortals.
- Use exports on the individual methods 
        -This leaves little help to tell people how to create a new naming service

Is there any shortcuts to getting all exports and sorting through them for mine? I'm planning to provide a Singletons interface that will return the service after sorting through this, but it defintely seems ugly compared to just slapping an import attribute on. What I want is to mark one as the default and use it only if no other exports exist. Let me guess, I can do that if I write my own programming model ;-).

Thanks for any insight. I understand how to sovle this through these strategies so I'm not stuck. I just thought it might be worthwhile to have a brief disucssion on whether there are other options and which approaches appear to be the best patterns.


Feb 13, 2009 at 6:14 AM

Kathleen, you are reminding me of another overdue blog post :-)

Defaults is something we've thought about quite a bit. We actually did a bunch of work in our Preview 3 bits specifically to support these scenarios. There are two common options we discuss. However, the easiest approach is to use the facilities we have provider for you, which I've laid out below.

Summary: Provide default export providers.

Our container has an aggregating export provider, we designed this provider so that it has a special behavior when it is querying for a single export. Basically whenever a query is issued to the aggregating ep for an export, say of contract ILogger, the provider will query each of it's children looking for a single. It will start at the top of the provider list and query. If it finds a provider that returns a single, it will return it, but if it finds a provider that returns multiple, it will keep querying until it finds a single. If it gets to the end of the list and has not found any, then an exception will be thrown. The container allows you to pass in addition to a catalog, additional export providers on it's construction. This means you can create a catalog export provider for a default catalog and pass it in.

With this approach, you can provide either overridable, or non-overridable defaults. If you put you "defaults" at the top of the list (i.e. pass your default catalog as the main catalog) then they will be non-overridable. Every query for a single import will hit your defaults first. If however you put your defaults at the end of the list, they will only be hit if a replacement does not exist. The advantage of this approach in general is it also allows queries for a single export to succeed in the presence of multiple exports, which normally fails with an exception due to cardinality.

Here are snippets of each.

a. Overridable defaults

var mainCatalog = new AssemblyCatalog(...) //main catalog
var defaultCatalog = new TypeCatalog(...) //pass in a colleciton of default parts w/default exports
var defaultEP = new CatalogExportProvider(defaultCatalog);
var container = new CompositionContainer(mainCatalog, defaultEP);
defaultEP.SourceProvider = container; //MUST do this.

So what we've done above is we created our main catalog and a default catalog. We then created a default export provider to wrap the default catalog. Next we passed the catalog, and default EP to the constructor of the container.

b. Non-Overridable defaults

var mainCatalog = new AssemblyCatalog(...) //main catalog
var defaultCatalog = new TypeCatalog(...) //pass in a colleciton of default parts w/default exports
var mainEP = new CatalogExportProvider(mainCatalog);
var container = new CompositionContainer(defaultCatalog, mainEP);
mainEP.SourceProvider = container; //MUST do this.

The lines in bold are the difference. Now we've put our default catalog first, and our main catalog after maing our defaults the first thing that is hit in a query.

Approach 2: Create a derived container.

If you have more complex rules, the best apporach is to simply derive from our container (we designed it to be easy to override), and then you override the GetExportsCore method. This core method gets hit every single time the container is queried, thus allowing you to intercept and provide your own custom filtering on the exports it returns by using your own LINQ query. For many scenarios like the one you quoted above though, I believe the first method is sufficient.