I have a .NET MVC application with 4 tiers. I'm trying to use dependency injection (via Ninject) but keep coming to the realization what I'm really thinking about is service location. My current problem is this:
I have dependency injection setup and working on many objects instantiated in my application layer (MVC 5 web app). Now, I'm coding the audit trail insert and would like it to be:
public ActionResult Edit(int id)
{
// ...
AuditTrail.LogVisit("Edit Screen", id);
return View();
}
AuditTrail
being the Entity Framework class representing the audit trail table (in a separate assembly/layer), and LogVisit
being a static method since I do not need the context of an existing audit trail record (this is an insert).
AuditTrail.LogVisit
creates a new DbContext for a short period of time to insert the record, and also requires the logged in user id. The logged in user id is available via the session, and I have exposed it as a strongly typed member of a class that is bound/injected using InRequestScope
- hoping that this would allow me to keep that value in the session but access it in higher tiers that are not aware of System.Web
or similar dependencies.
I'm having trouble getting the injected user id property/class in the Audit Trail
class since the creation of the DbContext is isolated to the AuditTrail.LogVisit
method. I'd like to avoid passing in the context root as this doesn't seem to be the best practice and creates other problems. I've looked into the factory extensions but from the examples you still don't have a static factory - you have an instance class that is provided a factory class instance.
I could avoid static methods but a) that just pushes the problem further down (how are the non-static helper classes instantiated inside the business layer?) and b) seems too limiting - can't use statics when all you need is the most relevant implementation located?
I could have the MVC application handle instantiating all the dependencies and pass them to the business layer method, but isn't that the problem dependency injection is trying to solve?
Dependency injection is about injecting instances, so static classes and/or methods cannot be used in this context. The whole idea is to create loosely-coupled code, and we do this be programming to an abstraction, which means interfaces. Interfaces do not work with static methods.
Values in a session have nothing to do with DI. The idea is that you separate definitions (interfaces) from their implementations (classes). An interface can be defined in a different layer than the corresponding implementing class. That is what makes it so powerful. You can define an interface in a fairly low-level layer, and put the implementation in a UI layer. An example of this is a user-context class, where the interface resides in a business layer, and the implementing class is in the web-layer, since that is where you have a session which you want to use in your implementation. DI makes it possible to use an implementation of this interface in your business layer, who knows nothing of web or session.
Back to your case. From what I take from your question, I can see the following.
In a lower layer (eg business layer), we define these:
public interface IUserContext {
int UserId { get; set; }
}
public interface IAuditTrail {
void LogVisit(string controller, int id);
}
Also, in the business (or data) layer, we define the implementation of the audit-trail.
public class AuditTrail : IAuditTrail {
public AuditTrail(
Func<DbContext> dbContextFactory,
IUserContext userContext
) {
// omitted: null guards
m_DbContextFactory = dbContextFactory;
m_UserContext = userContext;
}
private readonly DbContextFactory m_DbContextFactory;
private readonly IUserContext m_UserContext;
public void LogVisit(string controller, int id) {
using (var ctx = m_DbContextFactory()) {
var userId = m_UserContext.UserId;
// TODO: Log...
ctx.SaveChanges();
}
}
}
In the web application we define the implementation of the user-context.
public AspNetMvcUserContext : IUserContext {
private const UserIdSessionKey = "UserId";
public int UserId {
get { return (int)Session[UserIdSessionKey]; }
set { Session[UserIdSessionKey] = value; }
}
}
And finally, we use the above in a controller of your web application.
public class SomeController {
public SomeController(
IAuditTrail auditTrail
) {
// omitted: null guards
m_AuditTrail = auditTrail;
}
private readonly IAuditTrail m_AuditTrail;
public ActionResult Edit(int id) {
m_AuditTrail.LogVisit("Edit Screen", id);
return View();
}
}
Of course, you need to register all components above in your Ninject configuration.
It's clean, it's testable, and is pretty SOLID.