Service class. Dependency Injection

|

In this chapter we are going to check how to include Dependency Injection to our program. Current progress of our project is released with here, so if you are new then you can download current version and start working on it

Why we need service class?

Usually when we do web applications then stuff in Controllers gets really big. If it gets big then it is hard to manage it. Also it is hard to reuse content that is in Controllers. For example, if you want some logic to be presented in different Controllers/methods then you need to refactor logic somehow. General approach for this is to include Service methods so that Business Logic is separated from Controllers. This is what we are going to do.

Implementing Dependency Injection in MVC .NET C# application

So far we have working MVC application. What we want is to replace ‘context‘ object in our Controller with custom Service object. So in order to do so, with our project, we already have created Service Class Libary, inside that Service project, lets add 2 C# files which would be called ITodoService.cs and TodoService.cs. Snapshot of our Folder View is like this:

One of them starts with letter I, that represents interface, interface is like a contract that would tell what methods are available to call. The other one without I, eg TodoService, is implementation of that interface. So first, lets start with ItodoService.cs – it will be interface, lets add methods declarations over there like this:

// Services/ITodoService
using Domain;

namespace Services
{
    public interface ITodoService
    {
        Task<Todo?> GetByIdAsync(Guid? id);
        Task<IEnumerable<Todo>> GetAllAsync(string? searchKeyword, string? statusFilter, string? sortBy);
        Task DeleteAsync(Guid id);
        Task<bool> TodoExistsAsync(Guid id);
        Task<Todo> CreateAsync(Todo task);
        Task<Todo> UpdateAsync(Todo task);
    }
}

Alright, so we added 6 methods to our interface. Now we need to implement it in our TodoService class. So with TodoService class, lets add contract that TodoService must implement ItodoService properties:

namespace Services
{
    public class TodoService : ITodoService
    {
    }
}

If you did it like that then Compiler should tell you that ‘todoService’ does not implement interface member bla bla bla. So we need to implement methods. If you click on ‘Show potential fixes’ you get an option to implement interface. If you clicked on that you would see code like shown in this picture:

So there are lots of methods that needs to be implemented. Lets implement them. Below is the full code for TodoService:

using Domain;
using Microsoft.EntityFrameworkCore;

namespace Services
{
    public class TodoService : ITodoService
    {
        private AppDbContext _context;

        public TodoService(AppDbContext context)
        {
            _context = context;
        }
        public async Task<Todo> CreateAsync(Todo task)
        {
            if (task.DueDate < DateTime.Now)
            {
                throw new InvalidOperationException("DueDate cannot be in the past");
            }

            task.Id = Guid.NewGuid();
            task.DueDate = task.DueDate.ToUniversalTime();
            _context.Add(task);
            await _context.SaveChangesAsync();
            return task;
        }

        public async Task DeleteAsync(Guid id)
        {
            var todo = await _context.Todos.FindAsync(id) ?? throw new ArgumentException("Task not found");
            _context.Todos.Remove(todo);
            await _context.SaveChangesAsync();
        }

        public async Task<IEnumerable<Todo>> GetAllAsync(string? searchKeyword, string? statusFilter, string? sortBy)
        {
            var tasksQuery = _context.Todos.AsQueryable();
            if (!string.IsNullOrEmpty(searchKeyword))
            {
                var lowerSearch = searchKeyword.ToLower();
                tasksQuery = tasksQuery.Where(t => t.Title!.ToLower().Contains(lowerSearch)
                || t.Description!.ToLower().Contains(lowerSearch));
            }
            if (!string.IsNullOrEmpty(statusFilter))
            {
                if (Enum.TryParse(typeof(Status), statusFilter, out var status))
                {
                    tasksQuery = tasksQuery.Where(t => t.Status == (Status)status);
                }
            }
            switch (sortBy)
            {
                case "DueDate":
                    tasksQuery = tasksQuery.OrderBy(t => t.DueDate).ThenBy(t => t.Title);
                    break;
                case "Status":
                    tasksQuery = tasksQuery.OrderBy(t => t.Status).ThenBy(t => t.Title);
                    break;
                default:
                    tasksQuery = tasksQuery.OrderBy(t => t.Title);
                    break;
            }
            return await tasksQuery.ToListAsync();

        }

        public async Task<Todo?> GetByIdAsync(Guid? id)
        {
            if (id == null) return null;
            return await _context.Todos.FirstOrDefaultAsync(m => m.Id == id);
        }

        public async Task<bool> TodoExistsAsync(Guid id)
        {
            var todo = await _context.Todos.FindAsync(id);
            if (todo == null) { return false; }
            return true;
        }

        public async Task<Todo> UpdateAsync(Todo task)
        {
            task.DueDate = task.DueDate.ToUniversalTime();
            _context.Update(task);
            await _context.SaveChangesAsync();
            return task;
        }
    }
}

Very good, we are almost done with Service classes. All we have to do is change Controller and add Dependency injection to Program.cs

Service class to Controller

Lets add our Service class to our Controller. With this, lets remove context class from our Controller. Within our Controller, Lets first change the constructor:

    public class TodosController : Controller
    {
        private readonly ITodoService _service;

        public TodosController(ITodoService service)
        {
            _service = service;
        }
   // all other methods here
    }

Index method replacement

Alright, and now all the methods in controller that have _context object, lets replace them with our new Service. First one is Index method, lets replace it with this code:

        // GET: Todos
        public async Task<IActionResult> Index(string? searchKeyword, string? statusFilter, string? sortBy)
        {
            var todos = await _service.GetAllAsync(searchKeyword, statusFilter, sortBy);

            var viewModel = new TodoFilterViewModel
            {
                SearchKeyword = searchKeyword,
                StatusFilter = statusFilter,
                SortBy = sortBy,
                Todos = todos
            };
            return View(viewModel);
        }

Details method replacement

Next one is details, over there replace

var todo = await _context.Todos.FirstOrDefaultAsync(m => m.Id == id); with

        var todo = await _service.GetByIdAsync(id);

Create method replacement

On create method, you have GET and POST requests. Leave GET method as it is, but with POST method we can refactor code to be more compact one:

        // POST: Todos/Create
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("Id,Title,Description,DueDate,Status")] Todo todo)
        {
            if (ModelState.IsValid)
            {
                todo = await _service.CreateAsync(todo);
                return RedirectToAction(nameof(Index));
            }
            return View(todo);
        }

Edit method replacement

For Edit methods we have to replace both GET and POST ones. With GET method just replace line that contains _context with this code snippet:

        var todo = await _service.GetByIdAsync(id);

In POST method of edit, replace

                try
                {
                    todo.DueDate = todo.DueDate.ToUniversalTime();
                    _context.Update(todo);
                    await _context.SaveChangesAsync();
                }
                catch (DbUpdateConcurrencyException)
                {
                    if (!TodoExists(todo.Id))
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }

with this:

                try
                {
                    todo = await _service.UpdateAsync(todo);
                }
                catch (DbUpdateConcurrencyException)
                {
                    if (!await _service.TodoExistsAsync(todo.Id))
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }

Oh great, only Delete method more to go

Delete method replacement

On Delete GET method, replace GetById method just like we did it in Details and Edit GET methods. Replace _context call with this method :

var todo = await _service.GetByIdAsync(id);

And for DeleteConfirmed method, we included error handling to Service class, so that DeleteConfirmed we kinda have to replace entirely. So replace DeleteConfirmed with this code:

        // POST: TodoTasks/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(Guid id)
        {
            try
            {
                await _service.DeleteAsync(id);
            }
            catch (Exception ex)
            {
                return NotFound(ex.Message);
            }
            return RedirectToAction(nameof(Index));
        }

Add Dependency Injection to Program.cs

We are almost done with managing Dependency Injection. Only one thing to do: Tell our app what kind of implementation we have to use for our interface. For order to do that, lets look at our Program.cs file. Inside Program.cs, before var app = builder.Build();, lets include this line of code:

builder.Services.AddScoped<ITodoService, TodoService>();

This line of code tells that use TodoService implementation for ITodoService interface.

If everything got good then your program should work as it was before. Well done! Leave comments if you had problems, but for me it is running. Well done. Now lets finish it up by adding Unit Tests ->

🤩

Leave a Reply

Your email address will not be published. Required fields are marked *