Nano ASP.NET SaaS Boilerplate
Please allow a few seconds if the app is booting from a cold start.
Admin credentials (all tenants): admin@email.com / Password123!
Sample data resets every hour

Using Interfaces in ASP.NET Core (.NET 7). Tutorial and Guide

Interfaces in ASP.NET

So, what’s the deal with interfaces? To be honest, I never used them when I worked in ASP.NET MVC 5 projects. Anyone who worked on larger enterprise applications probably used them regularly, but for me it just seemed like needless extra code. By the way, if you are looking to develop enterprise grade applications that are maintainable with solid architecture, please check out the Nano ASP.NET Boilerplate.

It wasn’t until I moved on to .NET Core and learned about architecture and Dependency Injection, that I really fell in love with them.

Here are the main reasons to use interfaces, ranked by most useful:

  1. Swapping class implementations / improving maintainability (Dependency Injection)
  2. Enforcing contracts
  3. Tagging and automation

Download the sample project for this tutorial from Github here.

Essentially, an interface is like a blueprint of a class. The interface doesn’t contain ‘the meat’ of any code, just the signatures.

Let’s imagine that we have a web application where we can perform CRUD operations on a entity called Product. And for that functionality, we’ll create a service. Our service will consist of an interface and a class. Let’s look at an example.

This is the interface for the service.

using simpleApp.Models;
using simpleApp.Services.Application.ProductService.DTOs;

namespace simpleApp.Services.Application.ProductService
{
    public interface IProductService
    {
        IEnumerable<Product> GetAllProducts();
        Product GetProductById(int id);
        Product CreateProduct(CreateProductRequest request);
        bool DeleteProduct(int id);
    }
}

Notice that the structure looks similar to a class.

  • The first difference is the declaration of public interface instead of public class.
  • Second difference is that there is no code, only method signatures.

It’s convention to preface the name of the interface with a capital ‘I’. In this case, IProductService.

Now let’s take a look at what a class for this interface might look like.

using simpleApp.Models;
using simpleApp.Services.Application.ProductService.DTOs;

namespace simpleApp.Services.Application.ProductService
{
    public class ProductService : IProductService
    {
        private readonly ApplicationDbContext _context; // database context

        public ProductService(ApplicationDbContext context)
        {
            _context = context;
        }

        // get a list of all products
        public IEnumerable<Product> GetAllProducts()
        {
            var products = _context.Products.ToList();
            return products;
        }

        // get a single product
        public Product GetProductById(int id)
        {
            var product = _context.Products.Where(x => x.Id == id).FirstOrDefault();
            return product;
        }

        // create a new product
        public Product CreateProduct(CreateProductRequest request)
        {
            var product = new Product();
            product.Name = request.Name;
            product.Price = request.Price;

            _context.Add(product);
            _context.SaveChanges();

            return product;
        }


        // delete a product
        public bool DeleteProduct(int id)
        {
            var product = _context.Products.Where(x => x.Id == id).FirstOrDefault();

            if (product != null)
            {
                _context.Remove(product);
                _context.SaveChanges();
                return true;
            }
            return false;
        }
    }

}

This probably looks familiar, as its just a regular class. This is a CRUD style service class with methods to get a list of products, find by ID, add, or delete.

The usage of the interface comes into play right after public class ProductService. By using a colon : after the name of the class followed by the interface name IProductService, we are ‘implementing’ an interface.

So what does this mean? Well it means that, we are now bound to follow what has been outlined in the interface. Our class must have methods for get a list of products, find by ID, add, and delete and those methods must have the same signature (aka – method name, input parameters, and return output).

If we were to add a new method signature in the IProductService interface such as an update product method, our ProductService class won’t be fulfilling the specifications of that blueprint. We would need to add that method in our class and until we do so, we will get an error. We could however, have other methods in the ProductService class and that wouldn’t cause any compilation issues.

Ok so what’s the point of this? All we have done so far is write more code for no apparent benefit.  

The benefit is that we can add new classes to carry out the same objective as the original ProductService class. We can have multiple versions of ‘the same class’ and as long as they implement the same interface, we can easily swap them out. This prevents us from having to edit the currently implemented class when we want to make improvements on a live application.

Imagine that months later, our application has new business requirements for the product service. In that case, we could create a ProductServiceV2 class with some totally reworked code.  As long as the new class implements the same IProductService interface, it can be used by the application to fulfill the same role, like interchangeable parts. The only thing we will need to to when we are ready to implement the new ProductServiceV2 class, is change one line of code in the top level of our application (the service registration) to tell ASP to use the new class everywhere its implemented. That is in essence what Dependency Injection is. (More on that later).

This swap-ability principle is what allows Dependency Injection to work in .NET applications.

Perhaps you may not have to drastically rework code your business logic/CRUD services very often, but using interfaces is required for Dependency Injection, and it will make things more maintainable. Maintainability is very important in applications that evolve over time, so having an understanding of .NET architecture is essential before you hammer out the next application you build. The Nano ASP.NET Boilerplate can quickly advance your understanding of .NET architecture and provide a highly maintainable framework from the start. I encourage anyone at any level of .NET ability to check it out, review the documentation or try the live demo.

The need to swap in and out classes does happen quite often with infrastructure-style services which rely on third party packages like:

  • Mail services
  • Image / file upload services
  • Export to PDF, Excel, etc.

Let’s take a look at a more practical example.

You might have a web application which contains a mailer service. For simplicity, let’s say our application is only concerned about sending mail and the mail service has one method SendEmail, which returns true or false. It takes in a string message and string subject as parameters.

namespace simpleApp.Services.Infrastructure.Mailer
{
    public interface IMailerService
    {
        bool SendEmail(string message, string subject);
    }
}

Let’s say that we have been using Send In Blue, which is an email marketing platform with a .NET SDK, and our class looks like this. We can pretend there is a lot of code specific to how Send In Blue handles sending mail in this class. This class would likely have using statements from the SDK.

// --using statements from SendInBlue SDK

namespace simpleApp.Services.Infrastructure.Mailer
{
    public class SendInBlueService : IMailerService
    {
        // send method (required by interface)
        public bool SendEmail(string message, string subject)
        {
            // --
            // -- code specific to SendInBlue
            // --

            return true;
        }
    }
}

A service class could have more methods than just SendEmail. That’s totally fine, you can have more methods, properties, or whatever in your class as long as it fulfills the requirements of the interface. As long as a class has a SendEmail method which accepts the same parameters and returns a boolean value, its eligible to be used as the implementation of the IMailerService interface.

Now let’s say that Send In Blue decides to increase their prices and we want to use some alternative. Thanks to our interface, we can leave the SendInBlueService class as it is and create a new class alongside it called MailChimpService which contains code is specific to how Mail Chimp sends mail. As long as it has a method called SendEmail with a signature which matches that in the interface, we can use it.

// --using statements from Mailchimp SDK

namespace simpleApp.Services.Infrastructure.Mailer
{
    public class MailChimpService : IMailerService
    {
        // send method (required by interface)
        public bool SendEmail(string message, string subject)
        {
           
            // --
            // -- code specific to MailChimp
            // --

            return true;
        }
    }
}

Thanks to the pluggability interfaces grant us, we can build our new MailChimpService in a new class alongside the SendInBlueService class and when it’s ready, we’ll just change one line of code to tell our application to use MailChimpService wherever we are using the IMailService to send mail in our application.

So where is this one line of code? What is this magical little line which we have now spoken about twice?? It’s the service registration with Dependency Injection, typically happening in the top level of the application. This registration is where we specify the link between interface and class to implement, as well as the lifetime (transient, scoped, singleton) of the service. The injection points are in the constructors in various other classes scattered everywhere.

But before we go completely wild on Dependency Injection, lets look at two other ways interfaces are useful.

Interfaces are useful for enforcing requirements on classes. Actually, the two previous two examples already demonstrated this, but here is an example where the interfaces are used only for that purpose.  

Let’s say we have an application with lot of entity classes, and on some of these classes, we want to make sure that their creation date is persisted along with who created them (CreatedBy). For that we can create an interface like this:

namespace simpleApp.Models.Contracts
{
    public interface IAuditableEntity
    {
        public string CreatedBy { get; set; }
        public DateTime CreatedOn { get; set; }
    }
}

Now for any entity class that we want to adhere to that contract, we can implement the IAuditableEntity interface on them. For example, we have a Product entity type:

using simpleApp.Models.Abstract;
using simpleApp.Models.Contracts;

namespace simpleApp.Models
{
    // sample business entity
    public class Product : BaseEntity, IAuditableEntity, ISoftDelete
    {
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string CreatedBy { get; set; }
        public DateTime CreatedOn { get; set; }
        public bool IsDeleted { get; set; }
    }
}

By implementing the interface, we are now obligated to have a CreatedBy (String) and CreatedOn (Datetime) field in this class.

In .NET, classes can be inherited from higher level classes. In this example, we inherit from a BaseEntity which simply has an ID field. In these cases, add a comma and specify the interfaces to implement after the inherited class. By the way, .NET classes can only inherit from one higher level class at a time.

We can implement multiple interfaces though at the same time in the same class. Let’s say that for some of our entity types, we don’t want to actually delete the record from our database but rather just mark them as deleted. For that we might also have an ISoftDelete interface, with a Boolean field called IsDeleted.

Going back to our Product entity type, lets implement the ISoftDelete interface here as well.

We just add it with a comma after the first interface. With the interface implemented, we must add a new field to the class, IsDeleted and it must be a Boolean.

By the way, if you place the cursor in the ISoftDelete text, you can hold down control/command and press period to get a helper menu. Select implement interface and Visual Studio will scaffold the code signatures for you, which is useful if you have a lot of methods and properties in your interface.

And just like that we are using interfaces to enforce contracts. By why you ask? What is the point? Well aside from maintaining consistency, you can do some really cool things.

In Entity Framework for example, in our ApplicationDBContext, we can override the SaveChanges method and add in some automation code to stamp the date and the current user whenever an entity is saved, as long as it implements IAuditableEntity.

Same with ISoftDelete, we can tell our app to not delete the record if this entity is ‘of the group’ that is ‘tagged with’ ISoftDelete, and instead update the boolean field.

using Microsoft.EntityFrameworkCore;
using simpleApp.Models.Contracts;

namespace simpleApp.Models
{
    public class ApplicationDbContext : DbContext
    {

        // convention used by Entity Framework 
        public ApplicationDbContext(DbContextOptions options) : base(options)
        {
        }

        // -- create DbSets for all entity types to be managed with EF
        public DbSet<Product> Products { get; set; }


        // On Save Changes
        // -- automation with interfaces example
        // -- handle audit fields (createdOn, createdBy, modifiedBy, modifiedOn) and soft delete fields
        public override int SaveChanges()
        {
            // Auditable fields / soft delete on tables with IAuditableEntity
            foreach (var entry in ChangeTracker.Entries<IAuditableEntity>().ToList()) 
            {
                switch (entry.State)
                {
                    case EntityState.Added:
                        entry.Entity.CreatedBy = "the current user";
                        entry.Entity.CreatedOn = DateTime.UtcNow;
                        break;
                    case EntityState.Deleted:
                        // intercept delete requests, forward as modified on tables with ISoftDelete
                        if (entry.Entity is ISoftDelete softDelete) 
                        {
                            softDelete.IsDeleted = true;
                            entry.State = EntityState.Modified;
                        }
                        break;
                }
            }

            var result = base.SaveChanges();
            return result;
        }
    }
}

Don’t worry if you aren’t familiar with Entity Framework, the point is that you can use interfaces as groupings and perform automation on them. Which brings us to our last use case for interfaces.

The previous example did this actually, as you can see, these concepts are a bit overlapping

Where we had ChangeTracker.Entries<InterfaceName> we told Entity Framework to create a list of all entities that implemented IAuditableEntity and we looped over them to stamp some dates and usernames.

So, can we use interfaces for just their grouping power? Yes, and its common to do so. You can create a ‘blank’ interface and use it like a tag system. There are many occasions in .NET where you can pass an interface type to a registration or filter method and have whatever it is that you are doing, done to all those marked classes.

For example, with Dependency Injection, we will need to register all of our services in the top level of our application (the Service Container). That might go a lot smoother if we create blank interface markers like ITransientService, and IScopedService, and implement those interfaces in any service class we create. With that we could loop over and ‘auto-register’ our services. That is something fancy that the Nano ASP.NET boilerplate does, and if that sounds interesting go check it out (final shameless plug).

Now with a good understanding of interfaces, you can start using them in your code to make your application more flexible and easier to maintain.

The next article is about Dependency Injection, and we will see how interfaces serve as the building blocks in making that work.   

Leave a Reply

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

2 comments on “Using Interfaces in ASP.NET Core (.NET 7). Tutorial and Guide”

  1. jp2code says:

    I still don’t understand how I’m supposed to get the interface to pass to the constructor. Every example I see already has the code magically defined, like in `public ProductService(ApplicationDbContext context)`, but I never see anyone creating a new instance of ProductService, ie `var service = new ProductService(makeBelieveApplicationDbContextObj);` So, where does that “make believe application DB Context Object” come from? Please tell me! I’ve been searching for months and I can’t find it.

    1. Ryan says:

      Hi, thanks for your comment. I should append the article to show how you can use the ProductService in another class. For now though here is an example: Lets say you have another service, or controller called Invoice, and the invoice needs several services, one of which is our product service. In the constructor of the InvoiceController class, you would use the interface for the product service. So it would look something like this:

      public InvoiceController(IProductService productService)
      {
      _productService = productService
      }

      And then above the constructor, private readonly IProductService _productService;

      Now you have an instance of _productService for use within this invoice controller class.
      That’s how you use services. You always initialize them in a constructor using their interface.

Need an ASP.NET Boilerplate to build your next MVP?
Check out Nano ASP.NET multi-tenant SaaS boilerplate project and save weeks or months of development time.
Learn More