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:
[HttpGet] [Route("{blogPostId}")] [ResponseType(typeof(BlogPost))] publicasync Task<IHttpActionResult> GetBlogPost(int blogPostId) { // Logging logic to see what was provided to the Controller method.
if (blogPostId <= default(int)) { returnthis.BadRequest("Invalid Blog Post Id."); }
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:
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.
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.
publicasync 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);
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:
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; }
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:
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; }
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.