0
votes

I have an interface that represents a table in a 3rd party API. Each instance provides the ability to search a single table using forward-only cursors:

public interface ITable
{
    string TableName { get; }
    ICursor Search(string whereClause);
}

I have written a wrapper class to handle searching an ITable and returning an enumerable instead (it's a little more complex than that in reality, but sufficient for showing my issue):

public interface ITableWrapper
{
    IEnumerable<object> Search(string whereClause);
}

public class TableWrapper : ITableWrapper
{
    private ITable _table;

    public TableWrapper(ITable table)
    {
        _table = table;
    }

    public IEnumerable<Row> Search(string whereClause)
    {
        var cursor = _table.Search(whereClause);
        while(cursor.Next())
        {
            yield return cursor.Row;
        }
    }
}

I then have several repository classes that should have a table wrapper injected:

public class Table1Repository
{
    private ITableWrapper _table;

    public Table1Reposiroty(ITableWrapper table)
    {
        _table = table;
    }

    //repository methods to actually do things
}

Since each table will have its own wrapper, and repositories need the correct table injecting, my thought was to use named bindings on the tables and wrappers so that ninject provides the correct instance. Thus the above class would have NamedAttribute applied to the constructor argument, and the binding would be as follows:

public void NinjectConfig(IKernel kernel, ITableProvider provider)
{
    Bind<ITable>().ToMethod(ctx => provider.OpenTable("Table1")).Named("Table1").InSingletonScope();
    Bind<ITableWrapper>().ToMethod(ctx => new TableWrapper(ctx.ContextPreservingGet<ITable>("Table1"))).Named("Table1Wrapper").InSingletonScope();
}

My questions are:

  1. Is there a cleaner way to express this binding? I was thinking maybe a way to bind ITableWrapper once and have a new instance returned for each named ITable, with the repository constructor parameter attribute picking the named ITable for which it wants the ITableWrapper.
  2. If the ITable should never be used by anything, and everything should always use ITableWrapper, is it ok (or even recommended) to bind just ITableWrapper and have that combine both ToMethod contents:

public void NinjectConfig(IKernel kernel, ITableProvider provider)
{
    Bind<ITableWrapper>().ToMethod(ctx => new TableWrapper(provider.OpenTable("Table1"))).Named("Table1Wrapper").InSingletonScope();
}
1

1 Answers

1
votes

There's no Ninject-built-in way to provide metadata to Ninject by attribute. The only thing it supports is the ConstraintAttribute (and the NamedAttribute as a subclass). This can be used to select a specific binding, but it can't be used to provide parameters for a binding.

So, in case you don't want to add a lot of code, the easiest and most concise way is along of what you suggested yourself:

public static BindTable(IKernel kernel, ITableProvider tableProvider, string tableName)
{
    kernel.Bind<ITableWrapper>()
          .ToMethod(ctx => new tableWrapper(tableProvider.OpenTable(tableName))
          .Named(tableName);
}

(I've used the same string-id here for both table name and ITableWrapper name - this way you don't need to map them).

Also, i think it's better not to create a binding for ITable if you're not going to use it, anyway.

note: If you were to create the ITableWrapper by a factory (instead of ctor-injecting it), you could use parameters and a binding which reads the table-id from the parameter. Meaning a single binding would suffice.

Generic Solution

Now in case you're ok with adding some custom code you can actually achieve a generic solution. How? you add a custom attribute to replace the NamedAttribute which provides the table name. Plus you create a binding which reads the table name from this custom attribute. Let's say:

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class TableIdAttribute : Attribute
{
    public TableIdAttribute(string tableName)
    {
        TableName = tableName;
    }

    public string TableName { get; private set; }
}

let's implement an IProvider to capsule the added binding complexity (it would also work with a ToMethod binding):

internal class TableWrapperProvider : Provider<ITableWrapper>
{
    private readonly ITableProvider _tableProvider;

    public TableWrapperProvider(ITableProvider tableProvider)
    {
        _tableProvider = tableProvider;
    }

    protected override ITableWrapper CreateInstance(IContext context)
    {
        var parameterTarget = context.Request.Target as ParameterTarget;
        if (parameterTarget == null)
        {
            throw new ArgumentException(
                string.Format(
                    CultureInfo.InvariantCulture,
                    "context.Request.Target {0} is not a {1}",
                    context.Request.Target.GetType().Name,
                    typeof(ParameterTarget).Name));
        }

        var tableIdAttribute = parameterTarget.Site.GetCustomAttribute<TableIdAttribute>();
        if (tableIdAttribute == null)
        {
            throw new InvalidOperationException(
                string.Format(
                    CultureInfo.InvariantCulture,
                    "ParameterTarget {0}.{1} is missing [{2}]",
                    context.Request.Target,
                    context.Request.Target.Member,
                    typeof(TableIdAttribute).Name));
        }

        return new TableWrapper(_tableProvider.Open(tableIdAttribute.TableName));
    }
}

and here's how we use it (example classes):

public class FooTableUser
{
    public FooTableUser([TableId(Tables.FooTable)] ITableWrapper tableWrapper)
    {
        TableWrapper = tableWrapper;
    }

    public ITableWrapper TableWrapper { get; private set; }
}

public class BarTableUser
{
    public BarTableUser([TableId(Tables.BarTable)] ITableWrapper tableWrapper)
    {
        TableWrapper = tableWrapper;
    }

    public ITableWrapper TableWrapper { get; private set; }
}

and here's the bindings plus a test:

var kernel = new StandardKernel();
kernel.Bind<ITableProvider>().ToConstant(new TableProvider());
kernel.Bind<ITableWrapper>().ToProvider<TableWrapperProvider>();

kernel.Get<FooTableUser>().TableWrapper.Table.Name.Should().Be(Tables.FooTable);
kernel.Get<BarTableUser>().TableWrapper.Table.Name.Should().Be(Tables.BarTable);