DryIoc - Dependency Injection via Reflection

Introduction

My work inherited an ASP.NET WebApi Project from a contracted company. One of the first things added to the project was a Dependency Injection framework. DryIoc was selected for its speed in resolving dependencies.

In this post, I show why and how reflection was utilized to improve the Dependency Injection Registration code.

Architecture

First, let me provide a high-level overview of the architecture (using sample class names). The project is layered in the following way:

Controllers

Controllers are the entry-point for the code (just like all WebApi applications) and are structured something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[RoutePrefix("posts")]
public class BlogPostController : ProjectControllerBase
{
private readonly IBlogPostManager blogPostManager;

public BlogPostController(ILogger logger, IBlogPostManager blogPostManager)
: base(logger)
{
this.blogPostManager = blogPostManager;
}

[HttpGet]
[Route("{blogPostId}")]
[ResponseType(typeof(BlogPost))]
public async Task<IHttpActionResult> GetBlogPost(int blogPostId)
{
// Logging logic to see what was provided to the Controller method.

if (blogPostId <= default(int))
{
return this.BadRequest("Invalid Blog Post Id.");
}

BlogPost blogPost = await this.blogPostManager.GetBlogPost(blogPostId);

if (blogPost == null)
{
return this.NotFound();
}

return this.Ok(blogPost);
}

// ... Additional Service Methods
}

Controllers are responsible for ensuring sane input values are provided before passing the parameters through to the Manager layer. In doing so, the business logic is pushed downwards allowing for more re-use.

The ProjectControllerBase provides instrumentation logging and as a catch-all for any errors that may occur during execution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public abstract class ProjectControllerBase : ApiController
{
public ProjectControllerBase(ILogger logger)
{
this.Logger = logger;
}

protected ILogger Logger { get; }

public override async Task<HttpResponseMessage> ExecuteAsync(
HttpControllerContext controllerContext,
CancellationToken cancellationToken)
{
HttpRequestMessage requestMessage = controllerContext.Request;

// Logging request details

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();

try
{
return await base.ExecuteAsync(controllerContext, cancellationToken);
}
catch(ProjectException projectException)
{
// Contracting developers decided to throw exceptions for control-flow - this handles this case.

return requestMessage.CreateErrorResponse(HttpStatusCode.InternalServerError, projectException.Message);
}
catch(Exception exception)
{
// Log the exception.

return requestMessage.CreateErrorResponse(HttpStatusCode.InternalServerError, exception.Message);
}
finally
{
stopwatch.Stop();

// Log the time.
}
}
}

My goal is to refactor this at some point to remove the ILogger dependency.

Managers

Managers perform more refined validation and contain the business logic for the application. This allows for the business logic to be referenced by a variety of front-end applications (website, API, desktop application) easily.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class BlogPostManager : IBlogPostManager
{
public readonly IBlogPostRepository blogPostRepository;

public BlogPostManager(IBlogPostRepository blogPostRepository)
{
this.blogPostRepository = blogPostRepository;
}

public async Task<BlogPost> GetBlogPost(int blogPostId)
{
// Logging logic to see what was provided to the Manager method.

// Any additional validation logic can be performed here - such as ensuring the blog post exists.

BlogPostEntity blogPostEntity = await this.blogPostRepository.GetBlogPost(blogPostId);

// Any additional validation logic can be performed here - such as ensuring the blog post is not in Draft status.

if (blogPostEntity == null)
{
// Throw an exception (NullReference), or return null.
return null;
}

return new BlogPost()
{
// Mapping from BlogPostEntity to BlogPost model.
};
}

// ... Additional functionality
}

Each Manager is coded to an interface.

1
2
3
4
5
6
public interface IBlogPostManager
{
Task<BlogPost> GetBlogPost(int blogPostId);

// ... Additional functionality
}

By doing this, the Liskov Substitution Principle can be applied; allowing for flexible and isolated unit tests.

Repositories

Repositories act as the data access component for the project.

Initially, Entity Framework was used exclusively for data access. However; for read operations, Entity Framework is being phased for Dapper due to performance issues.

Entity Framework automatically uses an ORDER BY clause to ensure results are grouped. In some cases, this caused queries to time out. Often, this is a sign that the data model needs to be improved and/or that the SQL queries were too large (joining too many tables).

Additionally, our Database Administrators wanted read operations to use WITH (NOLOCK).

To the best of our knowledge, a QueryInterceptor would need to be used. This seemed to be counter-intuitive and our aggressive timeline would not allow for any time to tweak and experiment with the Entity Framework code.

For insert operations, Entity Framework is preferred.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class BlogPostRepository : IBlogPostRepository
{
private readonly BlogEntities blogEntities;
private readonly string databaseConnectionString;

public BlogPostRepository(BlogEntities blogEntities)
{
this.blogEntities = blogEntities;
this.databaseConnectionString = blogEntities.Database.ConnectionString;;
}

public async Task<BlogPostEntity> GetBlogPost(int blogPostId)
{
// Logging logic to see what was provided to the Repository method.

DynamicParameters sqlParameters = new DynamicParameters();
sqlParameters.Add(nameof(blogPostId), blogPostId);

StringBuilder sqlBuilder = new StringBuilder()
.AppendFormat(
@"SELECT
* -- Wildcard would not be used in actual code.
FROM blog_posts WITH (NOLOCK)
WHERE
blog_posts.blog_post_id = @{0}", nameof(blogPostId));

using (SqlConnection sqlConnection = new SqlConnection(this.databaseConnectionString))
{
await sqlConnection.OpenAsync();

// Logging logic to time the query.
BlogPostEntity blogPostEntity =
await sqlConnection.QueryFirstOrDefaultAsync(
sqlBuilder.ToString(),
sqlParameters);

return blogPostEntity;
}
}
}

Each Repository is coded to an interface.

1
2
3
4
5
6
public interface IBlogPostRepository
{
Task<BlogPostEntity> GetBlogPost(int blogPostId);

// ... Additional functionality
}

By doing this, the Liskov Substitution Principle can be applied; allowing for flexible and isolated unit tests.

DryIoc

DryIoc is fast, small, full-featured IoC Container for .NET

Registration

The Dependency Injection framework is registered during application start-up with OWIN:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class StartUp
{
public void Configuration(IAppBuilder appBuilder)
{
HttpConfiguration httpConfiguration = GlobalConfiguration.Configuration;

// ... Additional Set Up Configuration

DependencyInjectionConfiguration.Register(httpConfiguration);

// ... Additional Set Up Configuration

httpConfiguration.EnsureInitialized();

// ... Additional Start Up Configuration
}
}

The DependencyInjectionConfiguration class registers the container for the application to resolve dependencies using the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public static class DependencyInjectionConfiguration
{
public static void Register(HttpConfiguration httpConfiguration)
{
IContainer container = new Container().WithWebApi(httpConfiguration);

// ... Additional Registrations

DependencyInjectionConfiguration.RegisterEntityFrameworkContexts(container);

// ... Additional Registrations

DependencyInjectionConfiguration.RegisterManagers(container);
DependencyInjectionConfiguration.RegisterRepositories(container);

container.VerifyResolutions();
}

private static T CreateDbContext<T>()
where T : DbContext, new()
{
T context = new T();

context.Configuration.LazyLoadingEnabled = false;

// ... Set Up Database Logging: context.Database.Log = a => <logging mechanism>;

return context;
}

private static void RegisterEntityFrameworkContexts(IContainer)
{
container.Register<BlogEntities>(Reuse.InWebRequest, Made.Of(() => CreateDbContext<BlogEntities>()));
}

private static void RegisterManagers(IContainer)
{
// ... Additional Managers

container.Register<IBlogPostManager, BlogPostManager>(Reuse.InWebRequest);

// ... Additional Managers
}

private static void RegisterRepositories(IContainer)
{
// ... Additional Repositories

container.Register<IBlogPostRepository, BlogPostRepository>(Reuse.InWebRequest);

// ... Additional Repositories
}
}

Problems with this would occasionally arise when a developer introduced new Manager or Repository classes but did not remember to register instances of those classes with the Dependency Injection container. When this occurred, the compilation and deployment would succeed; but the following runtime error would be thrown when the required dependencies could not be resolved:

An error occurred when trying to create a controller of type ‘BlogPostController’. Make sure that the controller has a parameterless public constructor.

The generated error message does not help identify the underlying issue.

To prevent this from occurring, all Manager and Repository classes would need to automatically register themselves during start-up.

Reflection

To automatically register classes, reflection can be utilized to iterate over the assembly types and register all Manager and Repository implementations. Initially, this was done by loading the assembly containing the types directly from the disk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public static class DependencyInjectionConfiguration
{
public static void Register(HttpConfiguration httpConfiguration)
{
IContainer container = new Container().WithWebApi(httpConfiguration);

// ... Additional Registrations

DependencyInjectionConfiguration.RegisterEntityFrameworkContexts(container);

// ... Additional Registrations

DependencyInjectionConfiguration.RegisterManagersAndRepositories(container);

// ... Additional Registrations

container.VerifyResolutions();
}

// ... Additional Functions

private static bool IsInterfaceOrAbstractClass(Type exportedType)
{
return exportedType.IsInterface
|| exportedType.IsAbstract;
}


private static bool IsNotManager(Type exportedType)
{
return !exportedType.Name.EndsWith("Manager", StringComparison.InvariantCultureIgnoreCase);
}

private static bool IsNotRepository(Type exportedType)
{
return !exportedType.Name.EndsWith("Repository", StringComparison.InvariantCultureIgnoreCase);
}

// ... Additional Functions

private static void RegisterDependencies(IContainer container)
{
string assemblyPath = HttpContext.Current.Server.MapPath("~/bin/Dependencies.dll");
Assembly dependencyAssembly = Assembly.LoadFrom(assemblyPath);

foreach (Type exportedType in dependencyAssembly.GetExportedTypes())
{
// Skip registering items that are an interface or abstract class since it is
// not known if there is an implementation defined in this assembly.
if (DependencyInjectionConfiguration.IsInterfaceOrAbstractClass(exportedType))
{
continue;
}

// Skip registering items that are not a Manager, or Repository.
if (DependencyInjectionConfiguration.IsNotManager(exportedType)
&& DependencyInjectionConfiguration.IsNotRepository(exportedType))
{
continue;
}

string interfaceName = $"I{exportedType.Name}";
Type[] interfaceTypes = exportedType.GetInterfaces();

Type serviceType =
interfaceTypes.FirstOrDefault(
interfaceType =>
interfaceType.Name.Equals(interfaceName, StringComparison.InvariantCultureIgnoreCase))
?? exportedType;

container.Register(
serviceType,
exportedType,
Reuse.InWebRequest,
ifAlreadyRegistered: IfAlreadyRegistered.Keep);
}
}

// ... Additional Functions
}

While this works, it felt wrong to load the assembly from disk using a hard-coded path; especially when the assembly will be loaded by the framework automatically. To account for this, the code was modified in the following manner:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public static class DependencyInjectionConfiguration
{
public static void Register(HttpConfiguration httpConfiguration)
{
IContainer container = new Container().WithWebApi(httpConfiguration);

// ... Additional Registrations

DependencyInjectionConfiguration.RegisterEntityFrameworkContexts(container);

// ... Additional Registrations

DependencyInjectionConfiguration.RegisterManagersAndRepositories(container);

// ... Additional Registrations

container.VerifyResolutions();
}

// ... Additional Functions

private static bool IsInterfaceOrAbstractClass(Type exportedType)
{
return exportedType.IsInterface
|| exportedType.IsAbstract;
}


private static bool IsNotManager(Type exportedType)
{
return !exportedType.Name.EndsWith("Manager", StringComparison.InvariantCultureIgnoreCase);
}

private static bool IsNotRepository(Type exportedType)
{
return !exportedType.Name.EndsWith("Repository", StringComparison.InvariantCultureIgnoreCase);
}

// ... Additional Functions

private static void RegisterDependencies(IContainer container)
{
AssemblyName dependencyAssemblyName = Assembly.GetExecutingAssembly()
.GetReferencedAssemblies()
.FirstOrDefault(referencedAssembly => referencedAssembly.Name.Equals("Dependencies"));
Assembly dependencyAssembly = Assembly.Load(dependencyAssemblyName);

foreach (Type exportedType in dependencyAssembly.GetExportedTypes())
{
// Skip registering items that are an interface or abstract class since it is
// not known if there is an implementation defined in this assembly.
if (DependencyInjectionConfiguration.IsInterfaceOrAbstractClass(exportedType))
{
continue;
}

// Skip registering items that are not a Manager, or Repository.
if (DependencyInjectionConfiguration.IsNotManager(exportedType)
&& DependencyInjectionConfiguration.IsNotRepository(exportedType))
{
continue;
}

string interfaceName = $"I{exportedType.Name}";
Type[] interfaceTypes = exportedType.GetInterfaces();

Type serviceType =
interfaceTypes.FirstOrDefault(
interfaceType =>
interfaceType.Name.Equals(interfaceName, StringComparison.InvariantCultureIgnoreCase))
?? exportedType;

container.Register(
serviceType,
exportedType,
Reuse.InWebRequest,
ifAlreadyRegistered: IfAlreadyRegistered.Keep);
}
}

// ... Additional Functions
}

Unfortunately, there are no timing metrics available for measuring if there are any performance improvements for either implementation. With that said, the second implementation seems faster. This may be because the assembly is already loaded due to other registrations that occur before the reflection registration code is executed. For this reason, results may vary from project to project.

Overall, the solution works well and has limited the runtime error appearing only when a new Entity Framework context is added to the project.

Comments