Search code examples
c#asp.net-coredependency-injection

ASP.NET Dependency Injection Activator Utilities Error


The problem is:

An unhandled exception occurred while processing the request.
InvalidOperationException: Unable to resolve service for type 'CineMagicAPI.Controllers.PopularMoviesController' while attempting to activate 'CineMagic.Controllers.HomeController'.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.ThrowHelperUnableToResolveService(Type type, Type requiredBy)

CineMagicAPI> Movies.cs:

public class Movies
{
    public bool adult { get; set; }
    public string backdrop_path { get; set; }
    public List<int> genre_ids { get; set; }
    public int id { get; set; }
    public string original_language { get; set; }
    public string original_title { get; set; }
    public string overview { get; set; }
    public decimal popularity { get; set; }
    public string poster_path { get; set; }
    public DateTime release_date { get; set; }
    public string title { get; set; }
    public bool video { get; set; }
    public decimal vote_average { get; set; }
    public int vote_count { get; set; }
}

CineMagicAPI> PopularMoviesController.cs

using Microsoft.AspNetCore.Mvc;
using CineMagicAPI.Models;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

namespace CineMagicAPI.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class PopularMoviesController : ControllerBase
    {
        private readonly IHttpClientFactory _clientFactory;

        private readonly PopularMoviesController _popularMoviesService;

        private const string TMDB_API_KEY = "30b4c1980f6c33f5036366f3c4def27b"; //tmdb'ye kayıt olunup key alındı.

        public PopularMoviesController(IHttpClientFactory clientFactory, PopularMoviesController popularMoviesService)
        {
            _clientFactory = clientFactory;
            _popularMoviesService = popularMoviesService;
        } 

        [HttpGet]
        public async Task<List<Movies>> GetPopularMoviesAsync()
        {

            List<Movies> popularMovies = new List<Movies>();
            string endpoint = $"https://api.themoviedb.org/3/movie/popular?api_key={TMDB_API_KEY}";
            var client = _clientFactory.CreateClient();
            HttpResponseMessage response = await client.GetAsync(endpoint);
            if (response.IsSuccessStatusCode)
            {
                string json = await response.Content.ReadAsStringAsync();
                var data = JsonSerializer.Deserialize<TMDbResponse>(json);
                popularMovies = data?.results;
            }

            return popularMovies;
        }
    }
    public class TMDbResponse
    {
        public List<Movies> results { get; set; }
    }
}

CineMagic> HomeController.cs

using CineMagic.Models;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using CineMagicAPI.Controllers;

namespace CineMagic.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;
        private readonly PopularMoviesController _popularMoviesService;

        public HomeController(ILogger<HomeController> logger, PopularMoviesController popularMoviesService)
        {
            _logger = logger;
            _popularMoviesService = popularMoviesService;
        }

        public async Task<IActionResult> Index()
        {
            List<Movies> popularMovies = await _popularMoviesService.GetPopularMoviesAsync();
            return View(popularMovies);
        }
     
        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}

CineMagic>Program.cs

using CineMagicAPI.Controllers;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

I want to show popular movies on my homepage with the key I received from TMDB API. I have index html page but before that I get an error. How can I fix the error I shared above?


Solution

  • You are trying to inject one controller in another, but in the default configuration, controllers aren't registered in the DI Container. MVC creates controllers itself (not requesting them via the DI Container) and only requests its dependencies from the DI Container. That requires those dependencies to be registered but, again, controllers aren't registered (by default).

    A quick fix would be to add the following registration:

    builder.Services.AddControllersWithViews()
        .AddControllersAsServices();
    

    Although that would allow PopularMoviesController to be injected into your HomeController, it might still not work as expected. Whether or not this works depends on whether you make use of the request-specific state that MVC initializes for PopularMoviesController. This state is only initialized when it is created/resolved by MVC as root object, but not when it is injected into another object. Considering the code you posted, this will, currently, probably work, but it could easily break in the future if you make changes to PopularMoviesController.

    So a better fix is to prevent controllers from depending on each other. Instead, extract the shared logic out of PopularMoviesController and place it into a new class. Register that class into the DI Container and inject it as dependency into both PopularMoviesController and HomeController.

    For instance:

    [ApiController]
    [Route("[controller]")]
    public class PopularMoviesController : ControllerBase
    {
        private readonly PopularMoviesController _service;
    
        public PopularMoviesController(IPopularMoviesService service)
        {
            _service = service;
        } 
    
        [HttpGet]
        public async Task<List<Movies>> GetPopularMoviesAsync()
        {
            var movies = await _service.GetPopularMoviesAsync();
            return movies;
        }
    }
    
    public class HomeController : Controller
    {
        private readonly IPopularMoviesService _service;
    
        public HomeController(IPopularMoviesService service)
        {
            _service = service;
        }
    
        public async Task<IActionResult> Index()
        {
            var movies = await _service.GetPopularMoviesAsync();
            return View(movies);
        }
        ...
    }
    

    The logic from PopularMoviesController is now hid behind the IPopularMoviesService and both PopularMoviesController and HomeController depend on this. This IPopularMoviesService needs to be implemented and that would look like the following:

    public interface IPopularMoviesService
    {
        Task<List<Movies>> GetPopularMoviesAsync();
    }
    
    public class PopularMoviesService : IPopularMoviesService
    {
        private const string TMDB_API_KEY = "{your api key}";   
        private readonly IHttpClientFactory _clientFactory;
    
        public PopularMoviesController(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        } 
        
        public async Task<List<Movies>> GetPopularMoviesAsync()
        {
            List<Movies> popularMovies = new List<Movies>();
            var endpoint = $"https://api.themoviedb.org/3/movie/popular?api_key={TMDB_API_KEY}";
            // IMPORTANT: Do make sure HttpClient is disposed of.
            using (var client = _clientFactory.CreateClient())
            {
                HttpResponseMessage response = await client.GetAsync(endpoint);
                if (response.IsSuccessStatusCode)
                {
                    string json = await response.Content.ReadAsStringAsync();
                    var data = JsonSerializer.Deserialize<TMDbResponse>(json);
                    popularMovies = data?.results;
                }
    
                return popularMovies;
            }
        }    
    }
    

    This introduces a new PopularMoviesService class and IPopularMoviesService interface, which have to be registered in the DI Container as follows:

    builder.Services
        .AddTransient<IPopularMoviesService, PopularMoviesService>();