Component Metadata / Attribute Metadata

If you’re familiar with the Managed Extensibility Framework (MEF) you have probably seen examples using component metadata.

Metadata is information about a component, stored with that component, accessible without necessarily creating a component instance.

Adding Metadata to a Component Registration

Values describing metadata are associated with the component at registration time. Each metadata item is a name/value pair:

builder.Register(c => new ScreenAppender())
    .As<ILogAppender>()
    .WithMetadata("AppenderName", "screen");

The same thing can be represented in deployment-time configuration

{
  "components": [{
    "type": "MyApp.Components.Logging.ScreenAppender, MyApp",
    "services": [{
      "type": "MyApp.Services.Logging.ILogAppender, MyApp"
    }],
    "metadata": [{
      "key": "AppenderName",
      "value": "screen",
      "type": "System.String, mscorlib"
    }]
  }]
}

Consuming Metadata

Unlike a regular property, a metadata item is independent of the component itself.

This makes it useful when selecting one of many components based on runtime criteria; or, where the metadata isn’t intrinsic to the component implementation. Metadata could represent the time that an ITask should run, or the button caption for an ICommand.

Other components can consume metadata using the Meta<T> type.

public class Log
{
  readonly IEnumerable<Meta<ILogAppender>> _appenders;

  public Log(IEnumerable<Meta<ILogAppender>> appenders)
  {
    _appenders = appenders;
  }

  public void Write(string destination, string message)
  {
    var appender = _appenders.First(a => a.Metadata["AppenderName"].Equals( destination));
    appender.Value.Write(message);
  }
}

To consume metadata without creating the target component, use Meta<Lazy<T>> or the .NET 4 Lazy<T, TMetadata> types as shown below.

Strongly-Typed Metadata

To avoid the use of string-based keys for describing metadata, a metadata class can be defined with a public read/write property for every metadata item:

public class AppenderMetadata
{
  public string AppenderName { get; set; }
}

At registration time, the class is used with the overloaded WithMetadata method to associate values:

builder.Register(c => new ScreenAppender())
    .As<ILogAppender>()
    .WithMetadata<AppenderMetadata>(m =>
        m.For(am => am.AppenderName, "screen"));

Notice the use of the strongly-typed AppenderName property.

Registration and consumption of metadata are separate, so strongy-typed metadata can be consumed via the weakly-typed techniques and vice-versa.

You can also provide default values using the DefaultValue attribute:

public class AppenderMetadata
{
  [DefaultValue("screen")]
  public string AppenderName { get; set; }
}

If you are able to reference System.ComponentModel.Composition you can use the System.Lazy<T, TMetadata> type for consuming values from the strongly-typed metadata class:

public class Log
{
  readonly IEnumerable<Lazy<ILogAppender, LogAppenderMetadata>> _appenders;

  public Log(IEnumerable<Lazy<ILogAppender, LogAppenderMetadata>> appenders)
  {
    _appenders = appenders;
  }

  public void Write(string destination, string message)
  {
    var appender = _appenders.First(a => a.Metadata.AppenderName == destination);
    appender.Value.Write(message);
  }
}

Another neat trick is the ability to pass the metadata dictionary into the constructor of your metadata class:

public class AppenderMetadata
{
  public AppenderMetadata(IDictionary<string, object> metadata)
  {
    AppenderName = (string)metadata["AppenderName"];
  }

  public string AppenderName { get; set; }
}

Interface-Based Metadata

If you have access to System.ComponentModel.Composition and include a reference to the Autofac.Mef package you can use an interface for your metadata instead of a class.

The interface should be defined with a readable property for every metadata item:

public interface IAppenderMetadata
{
  string AppenderName { get; }
}

You must also call the RegisterMetadataRegistrationSources method on the ContainerBuilder before registering the metadata against the interface type.

builder.RegisterMetadataRegistrationSources();

At registration time, the interface is used with the overloaded WithMetadata method to associate values:

builder.Register(c => new ScreenAppender())
    .As<ILogAppender>()
    .WithMetadata<IAppenderMetadata>(m =>
        m.For(am => am.AppenderName, "screen"));

Resolving the value can be done in the same manner as for class based metadata.

Attribute-Based Metadata

The Autofac.Extras.AttributeMetadata package enables metadata to be specified via attributes. Core Autofac includes support to allow components to filter incoming dependencies using attributes.

To get attributed metadata working in your solution, you need to perform the following steps:

  1. Create Your Metadata Attribute
  2. Apply Your Metadata Attribute
  3. Use Metadata Filters on Consumers
  4. Ensure the Container Uses Your Attributes

Create Your Metadata Attribute

A metadata attribute is a System.Attribute implementation that has the System.ComponentModel.Composition.MetadataAttributeAttribute applied.

Any publicly-readable properties on the attribute will become name/value attribute pairs - the name of the metadata will be the property name and the value will be the property value.

In the example below, the AgeMetadataAttribute will provide a name/value pair of metadata where the name will be Age (the property name) and the value will be whatever is specified in the attribute during construction.

[MetadataAttribute]
public class AgeMetadataAttribute : Attribute
{
  public int Age { get; private set; }

  public AgeMetadataAttribute(int age)
  {
    Age = age;
  }
}

Apply Your Metadata Attribute

Once you have a metadata attribute, you can apply it to your component types to provide metadata.

// Don't apply it to the interface (service type)
public interface IArtwork
{
  void Display();
}

// Apply it to the implementation (component type)
[AgeMetadata(100)]
public class CenturyArtwork : IArtwork
{
  public void Display() { ... }
}

Use Metadata Filters on Consumers

Along with providing metadata via attributes, you can also set up automatic filters for consuming components. This will help wire up parameters for your constructors based on provided metadata.

You can filter based on a service key or based on registration metadata. This attribute based filtering can be performed without custom metadata attributes.

The KeyFilterAttribute, MetadataFilterAttribute, and WithAttributeFiltering extension method below can be found in the Autofac.Features.AttributeFilters namespace in the core Autofac package.

KeyFilterAttribute

The KeyFilterAttribute allows you to select a specific keyed service to consume.

This example shows a class that requires a component with a particular key:

public class ArtDisplay : IDisplay
{
  public ArtDisplay([KeyFilter("Painting")] IArtwork art) { ... }
}

That component will require you to register a keyed service with the specified name. You’ll also need to register the component with the filter so the container knows to look for it.

var builder = new ContainerBuilder();

// Register the keyed service to consume
builder.RegisterType<MyArtwork>().Keyed<IArtwork>("Painting");

// Specify WithAttributeFiltering for the consumer
builder.RegisterType<ArtDisplay>().As<IDisplay>().WithAttributeFiltering();

// ...
var container = builder.Build();

MetadataFilterAttribute

The MetadataFilterAttribute allows you to filter for components based on specific metadata values.

This example shows a class that requires a component with a particular metadata value:

public class ArtDisplay : IDisplay
{
  public ArtDisplay([MetadataFilter("Age", 100)] IArtwork art) { ... }
}

That component will require you to register a service with the specified metadata name/value pair. You could use the attributed metadata class seen in earlier examples, or manually specify metadata during registration time. You’ll also need to register the component with the filter so the container knows to look for it.

var builder = new ContainerBuilder();

// Register the service to consume with metadata.
// Since we're using attributed metadata, we also
// need to register the AttributedMetadataModule
// so the metadata attributes get read.
builder.RegisterModule<AttributedMetadataModule>();
builder.RegisterType<CenturyArtwork>().As<IArtwork>();

// Specify WithAttributeFilter for the consumer
builder.RegisterType<ArtDisplay>().As<IDisplay>().WithAttributeFiltering();

// ...
var container = builder.Build();

Ensure the Container Uses Your Attributes

The metadata attributes you create aren’t just used by default. In order to tell the container that you’re making use of metadata attributes, you need to register the AttributedMetadataModule into your container.

var builder = new ContainerBuilder();

// Register the service to consume with metadata.
// Since we're using attributed metadata, we also
// need to register the AttributedMetadataModule
// so the metadata attributes get read.
builder.RegisterModule<AttributedMetadataModule>();
builder.RegisterType<CenturyArtwork>().As<IArtwork>();

// ...
var container = builder.Build();

If you’re using metadata filters (KeyFilterAttribute or WithAttributeFiltering in your constructors), you need to register those components using the WithAttributeFiltering extension. Note that if you’re only using filters but not attributed metadata, you don’t actually need the AttributedMetadataModule. Metadata filters stand on their own.

var builder = new ContainerBuilder();

// Specify WithAttributeFilter for the consumer
builder.RegisterType<ArtDisplay>().As<IDisplay>().WithAttributeFiltering();
// ...
var container = builder.Build();