0

I have a question about Ninject, but before going directly to the question, I will explain the general scenario.

I have a Business interface called ITest__Business and its implementation Test__Business. This class depends on 3 interfaces: ITest__Repository, ITest2__Repository and IConnectionUtil. The interfaces ITest__Repository, ITest2__Repository and IConnectionUtil have classes that implement them with the default name (Test__Repository, Test2__Repository and ConnectionUtil respectively).

These domain classes represent business entities with dependencies of repository classes and a connection utility to handle the opening and closing of the database connection. The business class depends on both repositorys and a connectionutil. The connectionutil created within the business is shared by the 2 repositories (so that both the business and the repositories handle a single connection with DB).

This is the code of the aforementioned:

public interface ITest__Business {
    TablaUno ManageTablaUno(TablaUno tablaUno);
    IConnectionUtil ConnectionUtil {get; }
    IConnectionUtil ConnectionUtil2 {get; }
}
public class Test__Business : ITest__Business {
    private IConnectionUtil connUtil1;
    private ITest__Repository repo1;
    private ITest2__Repository repo2;
    public Test__Business(IConnectionUtil connUtil1, ITest__Repository repo1, ITest2__Repository repo2) {
        this.connUtil1 = connUtil1;
        this.repo1 = repo1;
        this.repo2 = repo2;
    }

    public TablaUno ManageTablaUno(TablaUno tablaUno){
       using (var scope = new TransactionScope()) {
           // Methods of repo1 and repo2 within transaction.
           //...
       }
    }

public interface ITest__Repository {
    IConnectionUtil ConnectionUtil { get; }
    TablaUno ManageTablaUno(TablaUno tablaUno);
}
public class Test__Repository : BaseRepository, ITest__Repository {
    public Test__Repository(IConnectionUtil connectionUtil) : base(connectionUtil) {
        // The connectionUtil is passed to the base class to retrieve the DbConnection
    }

    public TablaUno ManageTablaUno(TablaUno tablaUno) {
        // Invocation to stored procedure with the connection of the base class.
    }
}

public interface ITest2__Repository {/*Metodos propios*/}
public class Test2__Repository : BaseRepository, ITest2__Repository {
     // Logic similar to Test_Repository.
}

public interface IConnectionUtil {
    DbConnection Connection { get; }
    void Open();
    void Close();
}
public class ConnectionUtil : IConnectionUtil, IDisposable {
    private SqlConnection connection;

    public ConnectionUtil(string connStringKey) {
        var connString = WebConfigurationManager.ConnectionStrings[connStringKey].ConnectionString;
        connection = new SqlConnection(connString);
    }

    public DbConnection Connection => connection;

    public void Open() {
        try {
            connection.Open();
        } catch (Exception ex) {
            Debug.WriteLine($"Excepcion en ConnectionUtil.Open: {ex.Message}");
            throw;
        }
    }

    public void Close() {
        try {
            if (connection != null && connection.State == ConnectionState.Open) {
                connection.Close();
            }
        } catch (Exception ex) {
            Debug.WriteLine($"Excepcion en ConnectionUtil.Close: {ex.Message}");
            throw;
        }
    }

    /*Disposable Logic*/
}

And my Ninject module configuration:

public class ConsoleModule : NinjectModule {
    public override void Load() {
        Kernel.Bind<IConnectionUtil>().To<ConnectionUtil>().InCallScope().WithConstructorArgument("connStringKey", "Test1Connection");
        Kernel.Bind<ITest__Repository>().To<Test__Repository>().InTransientScope();
        Kernel.Bind<ITest2__Repository>().To<Test2__Repository>().InTransientScope();
        Kernel.Bind<ITestDb2__Repository>().To<TestDb2__Repository>().InTransientScope();
        Kernel.Bind<ITest__Business>().To<Test__Business>().InTransientScope();
    }
}

Subsequently, it was requested a new repository (repo3) and a new connectionutil (connUtil2) were added to the business. The connUtil2 that is created in the business is different from the connUtil1 (it has its own connection to a different DB) and it must be shared with the new repository (repo3). This to achieve interact with 2 different databases.

To achieve this, I created an attribute class ConnectionAttribute that has a constructor that sets the connection string that will be read from the .config file. This attribute must be added to connUtil2 and repo3 with the new connection string "Test2Connection", indicating that these are related. A repo1, repo2 and connUtil1 do not add this attribute, so that when solving the dependencies, if these target do not have this attribute, the original connection string "Test1Connection" will be used.

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)]
public sealed class ConnectionAttribute : Attribute {
    public string ConnectionString { get; private set; }

    public ConnectionAttribute(string connectionString) {
        ConnectionString = connectionString;
    }
}

public class Test__Business : ITest__Business {
    // Properties and methods are ignored for brevity.
    public Test__Business(IConnectionUtil connUtil1, [Connection("Test2Connection")] IConnectionUtil connUtil2,
        ITest__Repository repo1, ITest2__Repository repo2, [Connection("Test2Connection")] ITestDb2__Repository repo3) {
        this.connUtil1 = connUtil1;
        this.connUtil2 = connUtil2;
        this.repo1 = repo1;
        this.repo2 = repo2;
        this.repo3 = repo3;
    }
}

I also updated the ninject module:

public class ConsoleModule : NinjectModule {
    private IList<string> scopeList = new List<string>();

    public ConsoleModule() {
        foreach (ConnectionStringSettings connstr in WebConfigurationManager.ConnectionStrings) {
            scopeList.Add(connstr.Name);
        }
    }

    //public override void Load() {
    //    Kernel.Bind<IConnectionUtil>().To<ConnectionUtil>().InCallScope().WithConstructorArgument("connStringKey", "Test1");
    //    Kernel.Bind<ITest__Repository>().To<Test__Repository>().InTransientScope();
    //    Kernel.Bind<ITest2__Repository>().To<Test2__Repository>().InTransientScope();
    //    Kernel.Bind<ITestDb2__Repository>().To<TestDb2__Repository>().InTransientScope();
    //    Kernel.Bind<ITest__Business>().To<Test__Business>().InTransientScope();
    //}

    public override void Load() {
        Kernel.Bind<IConnectionUtil>().To<ConnectionUtil>().InScope(context => {
            var scopeCadena = string.Empty;
            if (context.Request.Target.Member.DeclaringType.Name.Contains("Repository")) {
                var pr = context.Request.ParentRequest.Target.GetCustomAttributes(typeof(ConnectionAttribute), false);
                var cadena = pr.Length == 0 ? "Test1" : ((ConnectionAttribute)pr[0]).ConnectionString;
                scopeCadena = scopeList.Single(x => x == cadena);
            }

            if (context.Request.Target.Member.DeclaringType.Name.Contains("Business")) {
                var attrs = context.Request.Target.GetCustomAttributes(typeof(ConnectionAttribute), false);
                var cadena = attrs.Length == 0 ? "Test1" : ((ConnectionAttribute)attrs[0]).ConnectionString;
                scopeCadena = scopeList.Single(x => x == cadena);
            }

            return scopeCadena;
        }).WithConstructorArgument("connStringKey", context => {
            var cadena = "Test1";
            if (context.Request.Target.Member.DeclaringType.Name.Contains("Repository")) {
                var pr = context.Request.ParentRequest.Target.GetCustomAttributes(typeof(ConnectionAttribute), false);
                cadena = pr.Length == 0 ? "Test1" : ((ConnectionAttribute)pr[0]).ConnectionString;
            }

            if (context.Request.Target.Member.DeclaringType.Name.Contains("Business")) {
                var attrs = context.Request.Target.GetCustomAttributes(typeof(ConnectionAttribute), false);
                cadena = attrs.Length == 0 ? "Test1" : ((ConnectionAttribute)attrs[0]).ConnectionString;
            }

            return cadena;
        });

        Kernel.Bind<ITest__Repository>().To<Test__Repository>().InTransientScope();
        Kernel.Bind<ITestDb2__Repository>().To<TestDb2__Repository>();
        Kernel.Bind<ITest__Business>().To<Test__Business>();
    }

This seems to work fine, but the problem is that every time I call Kernel.Get <ITest__Business>, the connUtil1 and connUtil2 are shared between business, and what I need is that they are related to the business scope, creating new connUtil1 and connUtil2 with each business instance.

How should I do the ninject configurations for this new case? Please help me.

===================================================================

UPDATE

On the other hand, although the answer to my question is given, I was curious to validate if it works if a new interface ITestDb3__Repository is added, to connect with a third database. Updating my business class constructor looks like this:

public Test__Business(IConnectionUtil connUtil1,
    ITest__Repository repo1, ITest2__Repository repo2, 
    [Connection("Test2Connection")] IConnectionUtil connUtil2, [Connection("Test2Connection")] ITestDb2__Repository repo3, 
    [Connection("Test3Connection")] IConnectionUtil connUtil3, [Connection("Test3Connection")] ITestDb2__Repository repo4) {
    this.connUtil1 = connUtil1;
    this.connUtil2 = connUtil2;
    this.connUtil3 = connUtil3;
    this.repo1 = repo1;
    this.repo2 = repo2;
    this.repo3 = repo3;
    this.repo3 = repo4;
}

What should happen is that there must be 3 different connectionUtils and repo4 shares the same connUtil3. But for this new scenario, connUtil3 equals connUtil2, since that the scope is only if it has the attribute present, but not its value. For this new scenario, how would the ninject configuration be?

===================================================================

UPDATE #2

What I need is to have a way to relate the repositories with the connectionutils within a business class.

A first case is when the business is required to connect to 2 databases and depend on 3 repositories so these internally execute call to stored procedures. The repositorys do not handle the connection, but this is done through the IConnectionUtil interface, which receives the connection string. For this case, the code would be the following:

/**** Test__Business constructor signature ****/
public Test__Business(IConnectionUtil connUtil1, ITest__Repository repo1_1, ITest2__Repository repo1_2, 
    IConnectionUtil connUtil2, ITestDb2__Repository repo2_1) { /* ... */ }

/**** Test__Business creation statements (equivalent to Kernel.Get<ITest__Business>) ****/
// repository1_1 and repository1_2 share the same connUtil1.
IConnectionUtil connUtil1 = new ConnectionUtil("TestConnection1"); // Connection string of the 1st database.
ITest__Repository repository1_1 = new Test__Repository(connUtil1);
ITest2__Repository repository1_2 = new Test2__Repository(connUtil1);

// repository2_1 shares the same connUtil2.
IConnectionUtil connUtil2 = new ConnectionUtil("TestConnection2"); // Connection string of the 2nd database.
ITestDb2__Repository repository2_1 = new TestDb2__Repository(connUtil2);

ITest__Business business = new Test__Business(connUtil1, repository1_1, repository1_2, connUtil2, repository2_1);

A second case would be if ask to modify the business so that it connects to a third database, now depending on a new IConnectionUtil and new ones repositorys that use this connectionUtil (suppose that 2 new repository interfaces were developed that invoked stored procedures of that 3rd new database), With this, the business constructor would look like this:

/**** Test__Business constructor signature ****/
public Test__Business(IConnectionUtil connUtil1, ITest__Repository repo1_1, ITest2__Repository repo1_2,
    IConnectionUtil connUtil2, ITestDb2__Repository repo2_1,
    IConnectionUtil connUtil3, ITestDb3_1__Repository repository3_1, ITestDb3_2__Repository repository3_2) { /* ... */}

/**** Test__Business creation statements (equivalent to Kernel.Get<ITest__Business>) ****/
// repository1_1 and repository1_2 share the same connUtil1.
IConnectionUtil connUtil1 = new ConnectionUtil("TestConnection1"); // Connection string of the 1st database.
ITest__Repository repository1_1 = new Test__Repository(connUtil1);
ITest2__Repository repository1_2 = new Test2__Repository(connUtil1);

// repository2_1 shares the same connUtil2.
IConnectionUtil connUtil2 = new ConnectionUtil("TestConnection2"); // Connection string of the 2nd database.
ITestDb2__Repository repository2_1 = new TestDb2__Repository(connUtil2);

// repository3_1 and repository3_2 share the same connUtil3.
IConnectionUtil connUtil3 = new ConnectionUtil("TestConnection3"); // Connection string of the 3rd database.
ITestDb3_1__Repository repository3_1 = new TestDb3_1__Repository(connUtil3);
ITestDb3_2__Repository repository3_2 = new TestDb3_1__Repository(connUtil3);

ITest__Business business = new Test__Business(connUtil1, repository1_1, repository1_2, connUtil2, repository2_1, connUtil3, repository3_1, repository3_2);

And so on if I need to add new connections to other databases. I need a way to relate, in the business constructor, which repositorys share the same connectionutil.

NOTE: Each repository and ConnectionUtil are unique for each business object, so 2 calls to Kernel.Get must produce different business objects, repositorys and ConnectionUtils.

Please help me.

Marlonchosky
  • 494
  • 5
  • 16

2 Answers2

0

The issue is clear: You use scopes, but these only exist once per connection string. However, you'd need a scope per business instance per connection string.

So how about extending on your previous InScallScope() binding that seemed to work just fine?

Keeping your custom attribute for specifying the connection, you can create multiple bindings for IConnectionUtil:

Kernel.Bind<IConnectionUtil>().To<ConnectionUtil>()
    .InCallScope()
    .WithConstructorArgument("connStringKey", "Test1Connection");

Kernel.Bind<IConnectionUtil>().To<ConnectionUtil>()
    .WhenAnyAncestorMatches(HasConnectionStringAttribute)
    .InCallScope()
    .WithConstructorArgument("connStringKey", RetrieveConnectinStringFromAttribute);

While I leave the implementation of bool HasConnectionStringAttribute(IContext context) and string RetrieveConnectinStringFromAttribute(IContext context) to you as an exercise ;-) (feel free to update the answer to make it complete!)

BatteryBackupUnit
  • 12,934
  • 1
  • 42
  • 68
  • Thank you very much for the help, very elegant response. I implemented both methods and it works for my case. On the other hand, although the answer to my question you already gave it to me, I was curious to validate if it works if I add a new interface ITestDb3__Repository to connect with a third database. I updated my question for this new scenario, could you help me please? – Marlonchosky Jan 10 '18 at 17:58
  • I understand that for this new scenario, I must create a custom scope, based on call scope but also for the name of the connection string. It is right? But I'm not sure how to do it – Marlonchosky Jan 10 '18 at 18:57
  • @Marlonchosky That's a possibility, yes. Looking at the [wiki](https://github.com/ninject/Ninject.Extensions.NamedScope/wiki/InCallScope) the relevant portion where your solution has to differ is `Contrasting it to InNamedScope, one would say its a limited version of InNamedScope() that automatically adds an anonymous Named Scope to the Root object generated by the Get()`. i.E. you would need to add a Named Scope for standard connection string and one per configured connection manager. This would also require to bind `IConnectionUtil` `InNamedScope(..)` per configured connection string. – BatteryBackupUnit Jan 11 '18 at 07:37
  • @Marlonchosky However, there's probably better alternative solutions. It's hard to reason about these unless you'd provide a specific example of what the DI container should do, basically manually writing showcase for what the DI should do (all necessary `new` statements). That'd make it much easier to reason about the optimal solution. – BatteryBackupUnit Jan 11 '18 at 07:41
  • Thank you very much. I updated my question at the end, below UPDATE #2 – Marlonchosky Jan 12 '18 at 04:13
  • Please help me. – Marlonchosky Jan 12 '18 at 23:01
0

After thinking and investigating the response for a long time, and thanks to the support of @BatteryBackupUnit, I found a way to create a custom scope, based on InCallScope.

Reviewing the InCallScope source code, I created a new extension method "InCallAndConnectionStringScope".

public static class NinjectExtensions {
    public static IBindingNamedWithOrOnSyntax<T> InCallAndConnectionStringScope<T>(this IBindingInSyntax<T> syntax, Func<IContext, string> getConnString) {
        return syntax.InScope(context => {
            var connString = getConnString(context);
            var ScopeParameterName = $"NamedScopeInCallScope_{connString}";
            var rootContext = context;
            while (!IsCurrentResolveRoot(rootContext) && rootContext.Request.ParentContext != null) {
                rootContext = rootContext.Request.ParentContext;
            }

            return GetOrAddScope(rootContext, ScopeParameterName);
        });
    }

    private static bool IsCurrentResolveRoot(IContext context) {
        return context.Request.GetType().FullName == "Ninject.Extensions.ContextPreservation.ContextPreservingResolutionRoot+ContextPreservingRequest";
    }

    private static object GetOrAddScope(IContext parentContext, string scopeParameterName) {
        var namedScopeParameter = GetNamedScopeParameter(parentContext, scopeParameterName);
        if (namedScopeParameter == null) {
            namedScopeParameter = new NamedScopeParameter(scopeParameterName);
            parentContext.Parameters.Add(namedScopeParameter);
        }

        return namedScopeParameter.Scope;
    }

    private static NamedScopeParameter GetNamedScopeParameter(IContext context, string scopeParameterName) {
        return context.Parameters.OfType<NamedScopeParameter>().SingleOrDefault(parameter => parameter.Name == scopeParameterName);
    } 
}

And the binding configuration:

public class ConsoleCustomModule : NinjectModule {
    public override void Load() {
        Bind<IConnectionUtil>().To<ConnectionUtil>().InCallAndConnectionStringScope(GetConnectionString)
            .WithConstructorArgument(GetConnectionString);

        Bind<ITestDb2__Repository>().To<TestDb2__Repository>();
        Bind<ITest__Repository>().To<Test__Repository>();
        Bind<ITest__Business>().To<Test__Business>();
    }

    private string GetConnectionString(IContext context) {
        var cadena = "Test1Connection";
        if (context.Request.Target.Member.DeclaringType.Name.Contains("Repository")) {
            var pr = context.Request.ParentRequest.Target.GetCustomAttributes(typeof(ConnectionAttribute), false);
            cadena = pr.Length == 0 ? "Test1Connection" : ((ConnectionAttribute)pr[0]).ConnectionString;
        }
        if (context.Request.Target.Member.DeclaringType.Name.Contains("Business")) {
            var attrs = context.Request.Target.GetCustomAttributes(typeof(ConnectionAttribute), false);
            cadena = attrs.Length == 0 ? "Test1Connection" : ((ConnectionAttribute)attrs[0]).ConnectionString;
        }

        return cadena;
    }
}

Having the following signature of the constructor:

public Test__Business(IConnectionUtil connUtil1, ITest__Repository repo1, ITest__Repository repo1_2,
    [Connection("Test2Connection")]IConnectionUtil connUtil2, [Connection("Test2Connection")] ITest__Repository repo2, [Connection("Test2Connection")] ITestDb2__Repository repo2_2, 
    [Connection("Test3Connection")]IConnectionUtil connUtil3, [Connection("Test3Connection")]ITestDb2__Repository repo3) {
    this.connUtil1 = connUtil1;
    this.connUtil2 = connUtil2;
    this.connUtil3 = connUtil3;
    this.repo1 = repo1;
    this.repo1_2 = repo1_2;
    this.repo2 = repo2;
    this.repo2_2 = repo2_2;
    this.repo3 = repo3;
}

You can validate objects according to the scope needed with the following tests:

[TestMethod]
public void Varias_Pruebas() {
    NinjectKernel.Init();
    var business1 = NinjectKernel.Kernel.Get<ITest__Business>();
    var business2 = NinjectKernel.Kernel.Get<ITest__Business>();

    // Different business objects.
    Assert.AreNotEqual(business1, business2);
    Assert.AreNotSame(business1, business2);

    // Different ConnectionUtil objects.
    Assert.AreNotEqual(business1.ConnectionUtil, business1.ConnectionUtil2);
    Assert.AreNotSame(business1.ConnectionUtil, business1.ConnectionUtil2);
    Assert.AreNotEqual(business1.ConnectionUtil, business1.ConnectionUtil2);
    Assert.AreNotSame(business1.ConnectionUtil, business1.ConnectionUtil2);
    Assert.AreNotEqual(business1.ConnectionUtil2, business1.ConnectionUtil3);
    Assert.AreNotSame(business1.ConnectionUtil2, business1.ConnectionUtil3);

    // Different repositories objects.
    Assert.AreNotEqual(business1.repo1, business1.repo1_2);
    Assert.AreNotSame(business1.repo1, business1.repo1_2);
    Assert.AreNotEqual(business1.repo1, business2.repo1);
    Assert.AreNotSame(business1.repo1, business2.repo1);
    Assert.AreNotEqual(business1.repo2, business1.repo2_2);
    Assert.AreNotSame(business1.repo2, business1.repo2_2);
    Assert.AreNotEqual(business1.repo2, business2.repo2);
    Assert.AreNotSame(business1.repo2, business2.repo2);

    // ConnectionUtils are shared between parameters with the same connString value of the connection attribute.
    Assert.AreEqual(business1.ConnectionUtil, business1.repo1.ConnectionUtil);
    Assert.AreSame(business1.ConnectionUtil, business1.repo1.ConnectionUtil);
    Assert.AreEqual(business1.ConnectionUtil, business1.repo1_2.ConnectionUtil);
    Assert.AreSame(business1.ConnectionUtil, business1.repo1_2.ConnectionUtil);
    Assert.AreEqual(business1.ConnectionUtil2, business1.repo2.ConnectionUtil);
    Assert.AreSame(business1.ConnectionUtil2, business1.repo2.ConnectionUtil);
    Assert.AreEqual(business1.ConnectionUtil2, business1.repo2_2.ConnectionUtil);
    Assert.AreSame(business1.ConnectionUtil2, business1.repo2_2.ConnectionUtil);
     Assert.AreEqual(business1.ConnectionUtil3, business1.repo3.ConnectionUtil);
    Assert.AreSame(business1.ConnectionUtil3, business1.repo3.ConnectionUtil);

    // No ConnectionUtils are shared between parameters with different connString value from the connection attribute.
    Assert.AreNotEqual(business1.ConnectionUtil, business1.repo2.ConnectionUtil);
    Assert.AreNotSame(business1.ConnectionUtil, business1.repo2.ConnectionUtil);
    Assert.AreNotEqual(business1.ConnectionUtil, business1.repo2_2.ConnectionUtil);
    Assert.AreNotSame(business1.ConnectionUtil, business1.repo2_2.ConnectionUtil);
    Assert.AreNotEqual(business1.ConnectionUtil2, business1.repo1.ConnectionUtil);
    Assert.AreNotSame(business1.ConnectionUtil2, business1.repo1.ConnectionUtil);
    Assert.AreNotEqual(business1.ConnectionUtil2, business1.repo1_2.ConnectionUtil);
    Assert.AreNotSame(business1.ConnectionUtil2, business1.repo1_2.ConnectionUtil);
    Assert.AreNotEqual(business1.ConnectionUtil2, business1.repo3.ConnectionUtil);
    Assert.AreNotSame(business1.ConnectionUtil2, business1.repo3.ConnectionUtil);
    Assert.AreNotEqual(business1.ConnectionUtil3, business1.repo1.ConnectionUtil);
    Assert.AreNotSame(business1.ConnectionUtil3, business1.repo1.ConnectionUtil);
    Assert.AreNotEqual(business1.ConnectionUtil3, business1.repo1_2.ConnectionUtil);
    Assert.AreNotSame(business1.ConnectionUtil3, business1.repo1_2.ConnectionUtil);
    Assert.AreNotEqual(business1.ConnectionUtil3, business1.repo2.ConnectionUtil);
    Assert.AreNotSame(business1.ConnectionUtil3, business1.repo2.ConnectionUtil);
    Assert.AreNotEqual(business1.ConnectionUtil3, business1.repo2_2.ConnectionUtil);
    Assert.AreNotSame(business1.ConnectionUtil3, business1.repo2_2.ConnectionUtil);
}
Marlonchosky
  • 494
  • 5
  • 16