Welcome to today’s post.
In today’s post, I will be going through some examples of how we can take an existing application written in .NET Core and convert it to make use of Primary Constructors. I will be going through a more specific example of a web application written in ASP.NET Core that will include some coverage of controllers and models and how they can be improved with the use of the primary constructor.
In the previous post, I gave an overview of what Primary Constructors are, reasons for their use, and how they compared to instance-based constructors and static constructors.
In the first section, I will show how to use primary constructors in user service classes.
Defining User Service Classes with Primary Constructors
The first area where we can use primary constructors in .NET Core applications is with classes that have been extended by interfaces. One of the key areas of the SOLID design principles is to use interfaces to extend classes (the interface segregation principle).
We can keep the principle intact and at the same time use primary constructors within our extended classes.
Below is the simplified interface that we have for a Book class that defines the two actions:
using System.Collections.Generic;
using System.Threading.Tasks;
using BookLoanWebAppWCAG.Models;
namespace BookLoanWebAppWCAG.Services
{
public interface IBookService
{
Task<List<BookViewModel>> GetBooks();
Task<BookViewModel> GetBook(int id);
}
}
For the book class, I will show the existing class implementation that uses dependency injection. Again, this satisfies another SOLID principle, which is dependency inversion.
using BookLoanWebAppWCAG.Data;
using BookLoanWebAppWCAG.Models;
using Microsoft.EntityFrameworkCore;
namespace BookLoanWebAppWCAG.Services
{
public class BookService : IBookService
{
readonly ApplicationDbContext _db;
private readonly ILogger _logger;
public BookService(
ApplicationDbContext db, ILogger<BookService> logger
)
{
_db = db;
_logger = logger;
}
/// <summary>
/// GetBooks()
/// </summary>
/// <returns></returns>
public async Task<List<BookViewModel>> GetBooks()
{
List<BookViewModel> books = await _db.Books.ToListAsync();
return books;
}
/// <summary>
/// GetBook()
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<BookViewModel> GetBook(int id)
{
BookViewModel book = await _db.Books.FindAsync(id);
return book;
}
}
}
As we can see, it uses an instance constructor which includes some dependent services like the data context and diagnostic logging:
public BookService(
ApplicationDbContext db, ILogger<BookService> logger)
{
_db = db;
_logger = logger;
}
What we will do now is to remove the instance constructor method and move the constructor parameters into the class declaration level, leaving the internal constructor properties as shown:
public class BookService(ApplicationDbContext db, ILogger<BookService> logger) : IBookService
{
readonly ApplicationDbContext _db = db;
private readonly ILogger _logger = logger;
readonly IBookService _bookService = bookService;
…
}
If we do not need to reference the internal properties at all, we can go further and improve it again as shown by removing the internal properties that are assigned to the constructor parameters:
public class BookService(ApplicationDbContext db, ILogger<BookService> logger) : IBookService
{
…
}
You are aware that the primary constructor parameters are not members of the class, they are only accessed by name, not by this keyword, so we can completely remove the internal properties that we used to store the primary constructor values since accessing the primary constructors within the class allows them to be stored with the class. After removing the internal properties, we access the primary constructor parameters directly within members and methods within the class. It leaves the class looking like this:
using BookLoanWebAppWCAG.Data;
using BookLoanWebAppWCAG.Models;
using Microsoft.EntityFrameworkCore;
namespace BookLoanWebAppWCAG.Services
{
public class BookService(ApplicationDbContext db, ILogger<BookService> logger) : IBookService
{
/// <summary>
/// GetBooks()
/// </summary>
/// <returns></returns>
public async Task<List<BookViewModel>> GetBooks()
{
List<BookViewModel> books = await db.Books.ToListAsync();
return books;
}
/// <summary>
/// GetBook()
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<BookViewModel> GetBook(int id)
{
BookViewModel book = await db.Books.FindAsync(id);
return book;
}
...
As you can see, the above class looks a lot tidier, cleaner, and maintains SOLID principles.
In the application startup, you can keep the service container defined for the Book class as it is:
builder.Services.AddTransient<IBookService, BookService>();
The next type of class we will convert to use primary constructors is one where one of the dependencies is with another user class and we use primary constructors within one of the model classes used within the main user class.
Primary Constructors with Simpler Model Classes
In this section, I will cover another example, where we convert another user class from using instance constructors to primary constructors.
During this conversion, we will have to deal with the following additional complexities:
- Creating a model class to use primary constructors that is implemented from a base model class.
- Use the model class that has primary constructors within the LoanService class.
- Integrating an additional service class Loan as a dependency, including the BookService class within the primary constructor.
In this section, I will show how to deal with parts 2 and 3. I will cover part 1 in the next section.
To start off, we have another interface for a LoanService class that defines the two actions:
using BookLoanWebAppWCAG.Models;
namespace BookLoanWebAppWCAG.Services
{
public interface ILoanService
{
Task<List<LoanReportViewModel>> GetBookLoans(int bookId);
Task<LoanViewModel> GetLoan(int id);
}
}
Before I get around to deal with the LoanService class, I will explain how to model class we use for the loan lists is extended to use primary constructors.
The model class used to return the list of book loans is derived from the following base class defined as shown:
#nullable disable
namespace BookLoanWebAppWCAG.Models
{
public class LoanViewModel
{
public int ID { get; set; }
public int BookID { get; set; }
public string LoanedBy { get; set; }
public DateTime DateLoaned { get; set; }
public DateTime DateDue { get; set; }
public DateTime DateReturn { get; set; }
public bool OnShelf { get; set; }
public DateTime DateCreated { get; set; }
public DateTime DateUpdated { get; set; }
public LoanViewModel() {}
}
}
We can then override the above base class with a reporting class that provides extra report fields: Title, and Author, that are from the book that is loaned:
#nullable disable
namespace BookLoanWebAppWCAG.Models
{
public class LoanReportViewModel: LoanViewModel
{
public string Title { get; set; }
public string Author { get; set; }
public LoanReportViewModel() {
}
}
}
We can then amend the derived reporting class with a primary constructor that provides an additional parameter, bookId that will be used to obtain data from the matching entry in the Book table and use the fields in a displayed report.
Below is the model class with the primary constructor:
#nullable disable
namespace BookLoanWebAppWCAG.Models
{
public class LoanReportViewModel(int bookId): LoanViewModel
{
new public int BookID { get; } = bookId;
public string Title { get; set; }
public string Author { get; set; }
public LoanReportViewModel(): this(0) {}
}
}
Notes:
- The additional parameter less constructor calls the primary constructor with a value of zero, so that this model can be instantiated with a parameter or without.
- Since the BookID field is already in the base class, I have had to override it with the following declaration as a new property that assigns it to the constructor parameter:
new public int BookID { get; } = bookId;
After the extra BookID field is set, we can then use it within the class to read data from the book data context. I will show how this is done in the next section when converting the user Loan class to use primary constructors.
Defining More Complex User Service Classes with Primary Constructors
In this section, I will show how we convert our LoanService class to use primary constructors and how to use the reporting model class that we defined with a primary constructor in the previous section.
Before I show the changes that are applied to the LoanService for primary constructors, I will show the original service that is below:
using BookLoanWebAppWCAG.Data;
using BookLoanWebAppWCAG.Models;
namespace BookLoanWebAppWCAG.Services
{
public class LoanService: ILoanService
{
readonly ApplicationDbContext _db;
private readonly ILogger _logger;
readonly IBookService _bookService;
public LoanService(IBookService bookService, ApplicationDbContext db,
ILogger<LoanService> logger)
{
_bookService = bookService;
_db = db;
_logger = logger;
}
public async Task<List<LoanReportViewModel>> GetBookLoans(int bookId)
{
var book = await _bookService.GetBook(bookId);
if (book == null)
return new List<LoanReportViewModel>();
var loans_list = new List<LoanReportViewModel>();
var loans = db.Loans.Where(l => l.BookID == bookId).ToList();
loans.ForEach(x =>
{
loans_list.Add(new LoanReportViewModel()
{
ID = x.ID,
BookID = x.BookID, // or bookId
Author = book.Author,
Title = book.Title,
LoanedBy = x.LoanedBy,
OnShelf = x.OnShelf,
DateDue = x.DateDue,
DateLoaned = x.DateLoaned,
DateReturn = x.DateReturn,
DateCreated = x.DateCreated,
DateUpdated = x.DateUpdated
});
}
);
return loans_list;
}
public Task<LoanViewModel> GetLoan(int id)
{
var loan = db.Loans.Where(l => l.ID == id).FirstOrDefault();
return loan!;
}
}
}
Note: When we create the LoanReportViewModel class, the BookID value is set to zero by default as it is a parameter less instance constructor. When the values are initialised, the BookID field is set with a value from the corresponding record’s BookID field value from the Loans table.
Next, I remove the instance constructor and define the class primary constructor with the same parameters from the instance constructor. The class declaration looks as shown:
public class LoanService(IBookService bookService, ApplicationDbContext db,
ILogger<LoanService> logger): ILoanService
{
…
}
I also remove the internal properties used to store the instance constructor parameters (as the primary constructor parameters will be stored in the class when first accessed).
The class now looks like this (with the body of the methods omitted as I will show those a little later):
using BookLoanWebAppWCAG.Data;
using BookLoanWebAppWCAG.Models;
namespace BookLoanWebAppWCAG.Services
{
public class LoanService(IBookService bookService, ApplicationDbContext db,
ILogger<LoanService> logger): ILoanService
{
public async Task<List<LoanReportViewModel>> GetBookLoans(int bookId)
{
var book = await bookService.GetBook(bookId);
if (book == null)
return new List<LoanReportViewModel>();
…
// Do some mapping on loan record to get report. See later!
…
return loans;
}
public Task<LoanViewModel> GetLoan(int id)
{
var loan = db.Loans.Where(l => l.ID == id).FirstOrDefault();
return loan!;
}
..
}
}
The method GetBookLoans() is optimized to include instances of the LoanReportViewModel class created with the primary constructor.
public async Task<List<LoanReportViewModel>> GetBookLoans(int bookId)
{
var book = await bookService.GetBook(bookId);
if (book == null)
return new List<LoanReportViewModel>();
var loans_list = new List<LoanReportViewModel>();
var loans = db.Loans.Where(l => l.BookID == bookId).ToList();
loans.ForEach(x =>
{
loans_list.Add(new LoanReportViewModel(bookId)
{
ID = x.ID,
Author = book.Author,
Title = book.Title,
LoanedBy = x.LoanedBy,
OnShelf = x.OnShelf,
DateDue = x.DateDue,
DateLoaned = x.DateLoaned,
DateReturn = x.DateReturn,
DateCreated = x.DateCreated,
DateUpdated = x.DateUpdated
});
}
);
return loans_list;
}
Note: As the primary constructor parameter BookID is set as an internal property within the model class, it does not need to be assigned to the BookID from the Loans table during the addition of entries to the collection in the iteration of Loans records.
Defining Primary Constructors for Controller Classes
In this section I will show how to convert an existing controller that has service dependencies into one that has a primary constructor.
Below is the original Book controller that uses dependency injection and instance constructors.
using Microsoft.AspNetCore.Mvc;
using BookLoanWebAppWCAG.Models;
using BookLoanWebAppWCAG.Services;
namespace BookLoanWebAppWCAG.Controllers
{
private IBookService _bookService;
public BookController(IBookService bookService)
{
this._bookService = bookService;
}
// GET: BookController
public ActionResult Index()
{
return List();
}
// GET: BookController
public ActionResult List()
{
BookListViewModel bookListView = new BookListViewModel()
{
ListHeading = "Book List!!",
BookList = _bookService.GetBooks().GetAwaiter().GetResult()
};
}
}
In the same way we did for the service classes, we can move the constructor parameter into the class definition as a primary constructor, remove the constructor method, and all internal properties used to store the dependencies from the constructor parameters.
We then reference the primary constructors directly from within the methods in the class.
using Microsoft.AspNetCore.Mvc;
using BookLoanWebAppWCAG.Models;
using BookLoanWebAppWCAG.Services;
namespace BookLoanWebAppWCAG.Controllers
{
public class BookController(IBookService bookService) : Controller
{
// GET: BookController
public ActionResult Index()
{
return List();
}
// GET: BookController
public ActionResult List()
{
BookListViewModel bookListView = new BookListViewModel()
{
ListHeading = "Book List!!",
BookList = bookService.GetBooks().GetAwaiter().GetResult()
};
return View("List", bookListView);
}
..
With the Loan controller, there is no difference in the way we convert the original controller to one that uses primary constructors.
Below is the original Loan controller:
using Microsoft.AspNetCore.Mvc;
using BookLoanWebAppWCAG.Models;
using BookLoanWebAppWCAG.Services;
namespace BookLoanWebAppWCAG.Controllers
{
public class LoanController : Controller
{
private ILoanService _loanService;
public LoanController(ILoanService loanService)
{
this._loanService = loanService;
}
// GET: LoanController
public ActionResult List(int bookId)
{
var loanListView = _loanService.GetBookLoans(bookId).GetAwaiter().GetResult();
return View("List", loanListView);
}
}
}
And below is the Loan controller with a primary constructor:
using Microsoft.AspNetCore.Mvc;
using BookLoanWebAppWCAG.Models;
using BookLoanWebAppWCAG.Services;
namespace BookLoanWebAppWCAG.Controllers
{
public class LoanController(ILoanService loanService) : Controller
{
// GET: LoanController
public ActionResult List(int id)
{
var loanListView = loanService.GetBookLoans(id).GetAwaiter().GetResult();
return View("List", loanListView);
}
}
}
Testing the Upgraded Application
The application was extended to include an additional loans report that is run off the list of books screen:

When the Loans link on any book record is clicked, it shows the following Loans Report, which is a list of loans for the selected book:

I will go through what I have noticed during a debugging session, and some of the observations will be surprising.
When running the application, following the changes to incorporate primary constructors in the Loan controller class, references to values from the primary constructors are valid and are usable to successfully execute dependent services.
Below is an excerpt of the debugger trace within a method within the Loan controller where the service value, loanService from the primary constructor is visible within the debugger locals window, however the instance cannot be expandable/viewable like other members within the Controller class:

This is one disadvantage of using the primary constructor within the Controller class – we cannot view the instance value of the primary constructor parameter!
When we use the original controller or service class with the internal properties assigned to the constructor parameters, we can certainly view the expanded values of the instance constructor parameters when assigned to internal properties.
This is what we see in the locals windows when debugging the BookService using vanilla dependency injection and assigned internal properties:

However, when we use primary constructors, instance properties of the service classes that we have declared within the primary constructor are not viewable.
In the BookService the local variables display as shown with the db parameter for the data context instance viewable:

In the LoanService the local variables display as shown, only db parameter for the data context instance is viewable, but the bookService parameter for the BookService is not viewable:

It seems that the penalty for the cleaner code is that we sacrifice the ability to debug service class dependencies from the primary constructor.
In the GetBookLoans() method within the LoanService class, I have also noticed that when the model class instance is created with the primary constructor, the overridden property BookID is shown with the base property, BookID set to zero as shown:

Instead of creating an additional property to store the parameter from the primary constructor, I declared a property with the new operator.
The above has been an overview of how to upgrade .NET Core applications to use primary constructors. We have also seen the advantages and disadvantages of using the feature during development and during development debugging.
That is all for today’s post.
I hope that you have found this post useful and informative.

Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.