Decoupling your code is something all software engineers should be striving for. Decoupling your code makes your code base easier to evolve over time. The evolution can be expected changes or even changes that one didn't expect to come about. If you've ever found yourself working on an application that is hard to change or when making a change unexpectedly breaks parts of your application then decoupling your application will help.
DI is a mechanical process; the process of passing dependencies to components/classes to consumers is dependency injection. A dependency is anything that your object relies on to complete it's tasks. For example, if you have to call the File class then your application is coupled to File. If you want to change the method of how to access data then your consuming object will have to change which makes it coupled to how File works.
The main method to inject dependencies is to have your dependencies be injected through the constructor of your consumer class. This isn't the only way to pass dependencies though; some other ways are via property injection and method injection.
Though not required, it is advised to have your consumers rely on (lowercase) interfaces and then implement the interfaces in your dependencies.
IoC is both a mechanical but also largely an abstract idea. IoC's mechanical process can be in reference to inverting the control of the creation and lifecycle of your objects but the abstract parts are in inverting the control of the interfaces/classes that are doing work.
Think of a class, lets make it a Human. We want this human to eat an orange. This might involve peeling the orange and then eating the orange.
var orange = new Orange();
Now what if we want the human to be able to eat an orange and a steak. The orange and steak should not be eaten the same way. A steak does not need to be peeled but should be cooked before eating. Yet both a orange and a steak from the human's perspective is the same thing....they both are food that need to be prepared and then eaten.
var steak = new Steak();
If we allow the Human to control the type of food it can it, then it will have to have multiple branches to handle each type of food; this is not ideal as the class will increase in size and complexity. Additionally, more complexity will arise when other classes also need to be able to work with all the types of food the human can consume (for example what if we had a stomach class that also needs to support the same type of food). This example illustrates that adding new types of food support will require touching multiple classes; increasing the complexity and risk.
To get around this we instead should invert the control so that the food IFood is injected into the Human class via method injection (but any DI method will work; make the class work first but try to ensure your interface make sense too). The human class would only need to maybe call IFood.Prepare and IFood.Consume as opposed to having to define in the class every way to prepare and consume food; we've inverted the control of how to eat food into the IFood interface. So every type of food, like orange, will implement IFood and the orange class will determine how the food needs to be prepared and consumed.
public void Eat(IFood food)
The Human class no longer needs to know how the food needs to be prepared or eaten but relies on the implementation to know how to be prepared and eaten. This makes adding more food easy as a new class just needs to implement the IFood interface and it can be used.
public class Orange : IFood
public class Steak : IFood
Practicing IoC/DI gives the programmer the ability to easily add functionality or even completely replace parts of the application without the consumers realizing anything has changed. This is because the interfaces give a level of abstraction that should clearly define the behavior that is required and as long as the new classes provide the same behavior, the underlying mechanics don't alter how consuming classes consume the dependency.
A DI container is a set of tools or a framework that helps facilitate the DI process. The composition root is where the DI container is created; this will usually be called from in Program.
Poor man's DI container is probably the easiest to use because it requires the programmer to just 'new' up the classes which is a concept many programmers are familiar with. The composition root would likely be newing all of your classes and dependencies and then calling the method on the outer most object.
Autofac or Castle Windsor are just two other frameworks that exist that accomplish the same thing. They vary in their maturity and available functionality but ultimately they are working to complete the same objectives. At its basic, a DI container will help you register and maintain the lifecycle of your objects. More mature containers will provide more advanced features to help registration, loading settings, and selecting the proper classes at runtime.
With the introduction of .NET Core, dependency injection has become a first class citizen. This means that we no longer are required to add a DI container because one is built into the framework but external containers can still override the built-in as needed.
Now that a DI container is built-in, we can leverage this for most scenarios and can save time because we don't have to bring in our own (I always have to relearn when I need to do this anyways).
Using the built-in container is preferred unless there are specific features required from your container. Another reason to prefer a third-party container is you are converting a legacy application to .NET Core and a container was already being used. Converting containers will introduce more risk than overriding the container and continuing to leverage your existing code.
Now that we understand and are actively applying IoC and DI, we can easily add automated unit tests. IoC and DI are the building blocks that make building unit testing easier. Unit tests help the programmer understand where and how they should decouple parts of their application. Automated unit tests (which hopefully we are using TDD to create) are tests that are automated to run often and quickly. These tests are meant to ensure that as we change code we don't break parts of our application unexpectedly (and when we do we find out quickly). Using DI and coding your implementations to interfaces means we can easily mock our dependencies which will make testing individual components easier.
Dependency injection and inversion of control is one of those topics in computer science that if you ask one hundred different programmers then you will likely get one hundred different answers. DI is the how and IoC is the why. IoC being an abstract idea; no one answer is the right answer, it all depends on the context and what your application requires.