Learn how to use dependency injections and interfaces in .NET
Why Dependency Injections
It is quite common to have an object that depends on another object to do its function, especially in an object-oriented environment like .NET. For instance, your constructor depends on your data context object. However, it can cause problems to always have to build dependencies first and then the objects that depend on them next. Also what if two classes need to access different versions of the dependency for example we might want to use the data context class both in a testing class and in the actual project but when using it in the testing context, we might want to set it up in such a way that we don't actually affect the database we could do this by adjusting the controller class but it would be easier if we could make that class initially more flexible.
The solution to these problems is dependency injections where rather than creating a dependency that a class depends on we give the class an interface to tell it what type of object to expect and then create a service that allows us to create dependencies as needed and inject them into the class.
Set up
Open the simple hello world project you made last week. Within MyWebApi (or whatever you named the project) add a models directory and create a Book.cs file within it that looks like this
namespaceMyWebApi.Models{publicclassBook {publicGuid Id {set; get;}publicstring? Name {set; get;} }}
We now need to install Microsoft EntityFrameworkCore to help set up a quick in-memory database. Like so
dotnet add package Microsoft.EntityFrameworkCore
Then we make a data folder and inside make a BookContext.cs file that will look like so
This is moving towards a simple in-memory database containing two books with randomly generated IDs. First, we will need to update our Program.cs file to use them.
Then update the Program.cs file to look like what follows.
usingMicrosoft.EntityFrameworkCore;usingMyWebApi.Data;var builder =WebApplication.CreateBuilder(args);// Add services to the container.// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbucklebuilder.Services.AddEndpointsApiExplorer();builder.Services.AddSwaggerGen();builder.Services.AddDbContext<BookContext>(options =>options.UseInMemoryDatabase("book db"));builder.Services.AddControllers();var app =builder.Build();using(var scope =app.Services.CreateScope())using(var db =scope.ServiceProvider.GetService<BookContext>()!){db.Database.EnsureCreated();}// Configure the HTTP request pipeline.if (app.Environment.IsDevelopment()){app.UseSwagger();app.UseSwaggerUI();}app.MapControllerRoute("default","api/[controller]/[action]");app.UseHttpsRedirection();app.Run();
This should give our BookContext the power to create in-memory databases and ensure they are created on app build.
Now let's update our controller to be a book controller that returns a list of books.
usingMicrosoft.AspNetCore.Mvc;usingMyWebApi.Data;usingMyWebApi.Models;namespaceMyWebApi.Controllers{ [ApiController] [Route("api/[controller]/[action]")]publicclassBookController:Controller{privatereadonlyBookContext _context;publicBookController(BookContext context) { _context = context; } [HttpGet]publicList<Book> GetAllBooks() {var result =_context.Books.ToList();return result; } [HttpGet]publicBookGetABook(string name) {var result =_context.Books.FirstOrDefault<Book>((book)=>book.Name== name);if (result ==null)thrownewException("book not found");return result; }}}
Give it a quick swagger test, and then let's look at a part of this code in more detail.
The constructor for this class (line 4) requires BookContext to function. This means these two objects are now tightly linked and we can't easily change one without changing the other.
To allow us more flexibility we are going to create something called an interface that we can feed in instead. An interface lets the constructor know what to expect but will not be as absolute as actually giving it the class.
The first step to do that is to go into our data directory and make a new file called IBookContext that looks like this.
This creates an interface and tells the class passed this that at run time it can expect something that contains a DbSet of books rather than the specific class passed to it before. Now we just need to tell all the other parts of our program to look for the interface rather than the object.
Finally, we do what we wanted to all along which is to make our book controller independent from the BookContext class by putting our interface in the constructor.
Besides making your work easier by having these two things looser it will also give you the ability to if you created an xUnit testing environment (like the one in the challenge repo), write code like this that substitutes fake data for the book context.
{ public class BookControllerTests { private readonly BookController _controller;publicBookControllerTests() {var fakeData =newList<Book>{newBook {Id =Guid.NewGuid(), Name ="Fake book1" },newBook {Id =Guid.NewGuid(), Name ="Fake book2" }}.AsQueryable(); var _context =newMock<IBookContext>(); _context.Setup(db =>db.Books).ReturnsDbSet(fakeData);_controller =newBookController(_context.Object); }[Fact] public voidGetAllBooks_Returns_Entire_List(){var expected =newList<Book>{newBook {Id =Guid.NewGuid(), Name ="Fake book1" },newBook {Id =Guid.NewGuid(), Name ="Fake book2" }}.AsQueryable();var result =_controller.GetAllBooks().ToList();Assert.Equivalent(expected, result);}[Fact]public voidGetABook_Returns_A_Book(){var expected =newBook {Id =Guid.NewGuid(), Name ="Fake book1" };var result =_controller.GetABook("Fake book1");Assert.Equivalent(expected, result); }}}
Note this part which would not be possible if the Book controller relied on a BookContext
var _context =newMock<IBookContext>(); _context.Setup(db =>db.Books).ReturnsDbSet(fakeData);_controller =newBookController(_context.Object);