Real-World Refactoring: Dependency Injecting a Non-ORM Repository for ASP.NET Core

A reader asked me how to implement DI with a non-ORM codebase. Today, we refactor some old code to include dependency injection for .NET Core.

Written by Jonathan "JD" Danylko • Last Updated: • Develop •
a collection of rubber bands on a table

One reader asked me about how to modernize the post titled "Creating a Repository Pattern Without An ORM" and implement it in ASP.NET Core with dependency injection (yep, it was four years ago).

This particular question got me wondering about the implementation of this code example.

Of course, since we weren't using an ORM of any kind, we could create a repository pattern to mimic a repository in Entity Framework.

While the implementation in my repository example (AdoRepository) didn't have an interface, I thought how can you use dependency injection on an abstract class?

In today's post, we'll take this code and start refactoring it from ASP.NET to ASP.NET Core while implementing dependency injection.

Time to Refactor!

Since you'll have a number of descendant repository classes from the abstracted AdoRepository, you want to keep the implementation details in the base class.

I know some feel composition is more important than inheritance, but we want to obey the DRY principle (Don't Repeat Yourself) and not repeat your code in your descendant classes. If that occurs, it may be time to move certain methods to the base class and work from there.

Also, our descendant repository classes (FaqRepository in this case) will have an interface attached to them and not on the abstract class. While the interface is the contract, the abstract is the implementation details for future classes.

With that said, our AdoRepository class won't change at all, but our FaqRepository class will.

Repository/FaqRepository.cs

public class FaqRepository: AdoRepository<Faq>, IFaqRepository
{
    public FaqRepository(string connectionString)
        : base(connectionString)
    {
    }

public List<Faq> GetAll()     {         // DBAs across the country are having strokes          //  over this next command!         using (var command = new SqlCommand("SELECT * FROM Faq"))         {             return GetRecords(command).ToList();         }     }

    public Faq GetById(string id)     {         // PARAMETERIZED QUERIES!         using (var command = new SqlCommand("SELECT * FROM Faq WHERE Id = @id"))         {             command.Parameters.Add(new SqlParameter("id", id));             return GetRecord(command);         }     }
    public override Faq PopulateRecord(SqlDataReader reader)     {         return new Faq         {             Question = reader.GetString(0),             Answer = reader.GetString(1)         };     } }

I know the Interface looks a little out of place, but it's absolutely necessary. If we are dependency injecting a FaqRepository, we absolutely need a "contract-to-concrete" relationship here. ;-)

Repository/IFaqRepository.cs

public interface IFaqRepository
{
    List<Faq> GetAll();
    Faq GetById(string id);
}

This makes our repositories easy to work with no matter what entity/model/table we work with in our application.

Also note the connection string in the constructor. We'll get to that in a bit as well.

Service It!

In the post, we didn't have a service layer because we didn't implement enough code at an application level.

The service layer looks similar to the repository interface, but has something different in the constructor.

Services/FaqService.cs

public class FaqService: IFaqService
{
    private readonly IFaqRepository _repository;

    public FaqService(IOptions<DataConnection> options)     {         var connection = options.Value;         _repository = new FaqRepository(connection.DefaultConnection);     }
    public List<Faq> GetAll()     {         return _repository.GetAll().ToList();     }
    public Faq GetById(string id)     {         return _repository.GetById(id);     } }

Services/IFaqService.cs

public interface IFaqService
{
    List<Faq> GetAll();
    Faq GetById(string id);
}

Our FaqService has an IOptions<DataConnection> passed into it. What the heck is that?

This was a lesson learned when first digging into ASP.NET Core and I found out there wasn't a ConfigurationManager available in Core.

The idea is to create an object to hold your appsettings.json and pass those options around through dependency injection in your Startup.cs file.

So my DataConnection object has the following structure:

Models/DataConnection.cs

public class DataConnection
{
    public string DefaultConnection { get; set; }
}

Riveting, I know. ;-)

The appsettings.json file looks like this:

{
  "DataConnection": {
    "DefaultConnection":  "Server=localhost" 
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*"
}

I didn't care about the other sections in the JSON file. I just want the DefaultConnection for my "entities" in my database.

The FaqService requires the IOptions<DataConnection> passed in through the constructor. How do we pass that?

We need to set up the configuration for our application in our startup.

Startup.cs

.
.
public
 void ConfigureServices(IServiceCollection services) {     services.Configure<CookiePolicyOptions>(options =>     {         // This lambda determines whether user consent for non-essential cookies is needed for a given request.         options.CheckConsentNeeded = context => true;         options.MinimumSameSitePolicy = SameSiteMode.None;     });
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    // Added - uses IOptions<T> for your settings.     services.AddOptions();
    // Added - Confirms that we have a home for our DataConnection     services.Configure<DataConnection>(Configuration.GetSection("DataConnection"));
    services.AddTransient<IFaqService, FaqService>();     services.AddTransient<IFaqRepository, FaqRepository>(); }
.
.

You'll notice all of the additions are in bold.

For the configuration to be dependency injected into your services, you need to add services.AddOptions(). We also need to tell .NET Core what to focus on when loading the configuration settings from appsettings.JSON so we point a DataConnection object to the appsettings section called DataConnection.

We also want to dependency inject our FaqService and FaqRepository anytime we request an IFaqService or IFaqRepository, respectively.

Bring it on Home[Controller]!

This is the easy part.

Since we hooked up the dependency injection for all of our services and repositories, we can simply create a constructor on our HomeController to accept an IFaqService and let .NET Core take care of the rest.

Controllers/HomeController.cs

public class HomeController : Controller
{
    private readonly IFaqService _service;

    public HomeController(IFaqService service)     {         _service = service;     }
    public IActionResult Index()     {         var faqs = _service.GetAll();                  return View(faqs);     }
    public IActionResult Privacy()     {         return View();     }
    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]     public IActionResult Error()     {         return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });     } }

.NET Core injects an instance of the FaqService into the HomeController's constructor allowing us to send the list of Faqs to our Index view.

While I didn't use a database with this example, making modifications to the code above with updated connection strings in the appsettings.json will make this code completely functional.

Conclusion

Using dependency injection, we can take our newly-created services and repositories and have .NET Core inject them into our constructors when we need them.

We've taken an older codebase and refactored it to use dependency injection so we can easily swap out an IFaqRepository with a different repository with minimal impact.

This gives the codebase a more modern approach even if we aren't using an ORM.

I relate Refactoring to building Rome: It doesn't happen all at once, it's an iterative thing. This is considered one iteration of a refactoring.

Until next time.

Was this a good refactoring? Did I miss something? What would you change? Post your comments below and let's discuss.

ASP.NET 8 Best Practices on Amazon

ASP.NET 8 Best Practices by Jonathan Danylko


Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

Order now on Amazon.com button

Picture of Jonathan "JD" Danylko

Jonathan "JD" Danylko is an author, web architect, and entrepreneur who's been programming for over 30 years. He's developed websites for small, medium, and Fortune 500 companies since 1996.

He currently works at Insight Enterprises as an Architect.

When asked what he likes to do in his spare time, he replies, "I like to write and I like to code. I also like to write about code."

comments powered by Disqus