June 2019

Volume 34 Number 6

[Patterns and Practices]

Super-DRY Development for ASP.NET Core

By Thomas Hansen

DRY is one of those really important software architecture acronyms. It means “Don’t Repeat Yourself” and articulates a critical principle to anyone who’s maintained a legacy source code project. That is, if you repeat yourself in code, you’ll find that every bug fix and feature update will have you repeating your modifications.

Code repetition reduces your project’s maintainability, and makes it more difficult to apply changes. And the more repetition you have, the more spaghetti code you’ll end up with. If, on the other hand, you avoid repetition, you can end up with a project that’s significantly easier to maintain and bug fix, and you’ll be a happier and more productive software developer, to boot. In short, committing to DRY code can help you create great code.

Once you start thinking in a DRY manner, you can bring this important architectural principle to a new level, where it feels as if your project is magically rising up from the ground—literally without having to apply any effort to create functionality. To the uninitiated, it may seem as if code is appearing out of thin air through the mechanisms of “super-DRY.” Great code is almost always tiny, but brilliant code is even tinier.

In this article, I’ll introduce you to the magic of super-DRY development and some of the tricks I’ve used over the years that can help you create your ASP.NET Core Web APIs with much less effort. Everything in this article is based on generalized solutions and the concept of DRY code, and uses only best practices from our industry. But first, some background theory.

CRUD, HTTP REST and SQL

Create, Read, Update and Delete (CRUD) is the foundational behavior of most data models. In most cases, your data entity types need these four operations, and in fact both HTTP and SQL are arguably built around them. HTTP POST is for creating items, HTTP GET is for reading items, HTTP PUT is for updating your items, and HTTP DELETE is for deleting items. SQL likewise evolves around CRUD with insert, select, update and delete. Once you give it some thought, it’s pretty obvious that it’s basically all about CRUD, assuming you don’t want to go “all in” and implement a CQRS architecture.

So you have the language mechanisms necessary to talk about HTTP verbs in a manner that they propagate all the way from the client’s HTTP layer, through your C# code, and into your relational database. Now, all you need is a generic way to implement these ideas through your layers. And you want to do it without repeating yourself, with a brilliant architectural foundation. So let’s get started.

First, download the code at github.com/polterguy/magic/releases. Unzip the file and open magic.sln in Visual Studio. Start your debugger, and notice how you already have five HTTP REST endpoints in Swagger UI. Where did these HTTP endpoints originate? Well, let’s look at the code, because the answer to that question may surprise you.

Look Mom, No Code!

The first thing you’ll notice as you start browsing the code is that the ASP.NET Core Web project itself is literally empty. This is possible due to an ASP.NET Core feature that allows you to dynamically include controllers. If you want to see the internals behind this, you can check out the Startup.cs file. Basically, it’s dynamically adding each controller from all the assemblies in your folder into your AppDomain. This simple idea allows you to reuse your controllers and to think in a modularized way as you compose your solutions. The ability to reuse controllers across multiple projects is step 1 on your way to becoming a super-DRY practitioner.

Open up the web/controller/magic.todo.web.controller project and look at the TodoController.cs file. You’ll notice that it’s empty. So where did these five HTTP REST endpoints come from? The answer is through the mechanism of object-oriented programming (OOP) and C# generics. The TodoController class inherits from CrudController, passing in its view model and its database model. In addition, it’s using dependency injection to create an instance of the ITodoService, which it hands over to the CrudController base class.

Because the ITodoService interface inherits from ICrudService with the correct generic class, the CrudController base class happily accepts your service instance. In addition, at this point it can already use the service polymorphistically, as if it were a simple ICrudService, which of course is a generic interface with parameterized types. This provides access to five generically defined service methods in the CrudController. To understand the implications of this, realize that with the following simple code you’ve literally created all the CRUD operations you’ll ever need, and you’ve propagated them from the HTTP REST layer, through the service layer, into the domain class hierarchy, ending up in the relational database layer. Here’s the entire code for your controller endpoint:

[Route("api/todo")]
public class TodoController : CrudController<www.Todo, db.Todo>
{
  public TodoController(ITodoService service)
    : base(service)
  { }
}

This code gives you five HTTP REST endpoints, allowing you to create, read, update, delete and count database items, almost magically. And your entire code was “declared” and didn’t contain a single line of functionality. Now of course it’s true that code doesn't generate itself, and much of the work is done behind the scenes, but the code here has become “Super-DRY.” There’s a real advantage to working with a higher level of abstraction. A good analogy would be the relationship between a C# if-then statement and the underlying assembly language code. The approach I’ve outlined is simply a higher level of abstraction than hardcoding your controller code.

In this case, the www.Code type is your view model, the db.Todo type is your database model and ITodoService is your service implementation. By simply hinting to the base class what type you want to persist, you have arguably finished your job. The service layer again is equally empty. Its entire code can be seen here:

public class TodoService : CrudService<Todo>, ITodoService
{
  public TodoService([Named("default")] ISession session)
    : base(session, LogManager.GetLogger(typeof(TodoService)))
  { }
}

Zero methods, zero properties, zero fields, and yet there’s still a complete service layer for your TODO items. In fact, even the service interface is empty. The following shows the entire code for the service interface:

public interface ITodoService : ICrudService<Todo>
{ }

Again, empty! Yet still, sim salabim, abra kadabra, and you have a complete TODO HTTP REST Web API application. If you open up the database model, you’ll see the following:

public class Todo : Model
{
  public virtual string Header { get; set; }
  public virtual string Description { get; set; }
  public virtual bool Done { get; set; }
}

Once again, there’s nothing here—just a couple of virtual properties and a base class. And yet, you’re able to persist the type into your database. The actual mapping between your database and your domain type occurs in the TodoMap.cs class inside the magic.todo.model project. Here, you can see the entire class:

public class TodoMap : ClassMap<Todo>
{
  public TodoMap()
  {
    Table("todos");
    Id(x => x.Id);
    Map(x => x.Header).Not.Nullable().Length(256);
    Map(x => x.Description).Not.Nullable().Length(4096);
    Map(x => x.Done).Not.Nullable();
  }
}

This code instructs the ORM library to use the todos table, with the Id property as the primary key, and sets a couple of additional properties for the rest of the columns/properties. Notice that when you started this project, you didn’t even have a database. This is because NHibernate automatically creates your database tables if they don’t already exist. And because Magic by default is using SQLite, it doesn’t even need a connection string. It’ll automatically create a file-based SQLite database at a relative file path, unless you override its connection settings in appsettings.config to use MySQL or MSSQL.

Believe it or not, your solution already transparently supports almost any relational databases you can imagine. In fact, the only line of actual code you would need to add to make this thing function can be found in the magic.todo.services project, inside the ConfigureNinject class, which simply binds between the service interface and the service implementation. So, arguably, you added one line of code, and got an entire application as the result. Following is the only line of actual “code” used to create the TODO application:

public class ConfigureNinject : IConfigureNinject
{
  public void Configure(IKernel kernel, Configuration configuration)
  {
    // Warning, this is a line of C# code!
    kernel.Bind<ITodoService>().To<TodoService>();
  }
}

We’ve become super-DRY magicians, through intelligent use of OOP, generics and the principles of DRY. So the question begs: How can you use this approach to make your code better?

The answer: Start out with your database model and create your own model class, which can be done either by adding a new project inside your model folder, or by adding a new class to the existing magic.todo.model project. Then create your service interface in the contracts folder. Now implement your service in your services folder, and create your view model and controller. Make sure you bind between your service interface and your service implementation. Then if you choose to create new projects, you’ll have to make sure ASP.NET Core loads your assembly by adding a reference to it in your magic.backend project. Notice that only the service project and the controller need to be referenced by your back end.

If you chose to use the existing projects, the last part isn’t even necessary. One simple line of actual code to bind between your service implementation and your service interface, and you’ve created an entire ASP.NET Core Web API solution. That’s one mighty line of code if you ask me. You can see me go through the entire process for an earlier version of the code in my “Super DRY Magic for ASP.NET Core” video at youtu.be/M3uKdPAvS1I.

Then imagine what occurs when you realize that you can scaffold this code, and automatically generate it according to your database schema. At this point, your computer scaffolding software system is arguably doing your coding, producing a perfectly valid Domain-Driven Design (DDD) architecture in the process.

No Code, No Bugs, No Problem

Most scaffolding frameworks apply shortcuts, or prevent you from extending and modifying their resulting code, such that using them for real-world applications becomes impossible. With Magic, this shortcoming is simply not true. It creates a service layer for you, and it uses dependency injection to inject a service interface to your controller. It also produces perfectly valid DDD patterns for you. And as you’ve created your initial code, every single part of your solution can be extended and modified as you see fit. Your project perfectly validates to every single letter in SOLID.

For instance, in one of my own solutions, I have a POP3 server fetcher thread in my service, declared for an EmailAccount domain model type. This POP3 service stores emails into my database from my POP3 server, running on a background thread. When an email is deleted, I want to make sure I also physically delete its attachments in storage, and if the user deletes an EmailAccount, I obviously want to delete its associated emails.

The code in Figure 1 shows how I’ve overridden the deletion of an EmailAccount, which also should delete all emails and attachments. For the record, it uses Hibernate Query Language (HQL) to communicate with the database. This ensures that NHibernate will automatically create the correct SQL syntax, depending on to which database it’s physically connected.

Figure 1 Overriding the Deletion of an EmailAccount

public sealed class EmailAccountService : CrudService<EmailAccount>,
  IEmailAccountService
{
  public EmailAccountService(ISession session)
    : base(session, LogManager.GetLogger(typeof(EmailAccountService)))
  { }
  public override void Delete(Guid id)
  {
    var attachments = Session.CreateQuery(
      "select Path from EmailAttachment where Email.EmailAccount.Id = :id");
    attachments.SetParameter("id", id);
    foreach (var idx in attachments.Enumerable<string>())
    {
      if (File.Exists(idx))
        File.Delete(idx);
    }
    var deleteEmails = Session.CreateQuery(
      "delete from Email where EmailAccount.Id = :id");
    deleteEmails.SetParameter("id", id);
    deleteEmails.ExecuteUpdate();
    base.Delete(id);
  }
}

Doing the Math

Once you start philosophizing around these ideas, inspiration strikes. For instance, imagine a scaffolding framework built around Magic. From a mathematical point of view, if you have a database with 100 tables, each with an average of 10 columns, you’ll find that the cost in terms of total lines of code can add up fast. For instance, to wrap all of these tables into an HTTP REST API requires seven lines of code per service interface, while 14 lines of code are needed per table for each service and 19 lines are needed per table for each controller. Figure 2 runs down the elements involved and the lines of code required.

Figure 2 Adding Up the Cost in Code

Component Contracts Average Lines of Code Total Lines of Code
Service interfaces 100 7 700
Services 100 14 1,400
Controllers 100 19 1,900
Service interface and implementation 100 1 100
View models 100 17 1,700
Database models 100 17 1,700
Database mappings 100 20 2,000
Total Lines of Code: 9,500

 

After all is said and done, you’re looking at 9,500 lines of code. If you build a meta service capable of extracting an existing data­base schema, it becomes pretty obvious that you can generate this code using scaffolding—arguably avoiding any coding at all, yet still producing 9,500 lines of perfectly architected code, easily extended, using all the relevant design patterns and best practices. With only two seconds of scaffolding, your computer has done 80 percent of your work.

All you have to do now is go through the results of the scaffolding process, and override methods for your services and controllers for the domain types that require special attention for whatever reason. You’re done with your Web API. Because the controller endpoints all have the exact same structure, duplicating this scaffolding process in the client layer is as easy as reading the API JSON declaration files generated by Swagger. This enables you to create your service layer for something such as Angular or React. And all this because your code and your Web API have predictable structures, based on generalization principles and the avoidance of repetition.

To put this in perspective, you’ve managed to create an HTTP REST Web API project that’s probably twice as large as the open source Sugar CRM project in complexity, and you did 80 percent of the work in seconds. You’ve facilitated a software factory assembly line that’s based on standardization of components and reuse of structure, while making the code for all your projects much easier to read and maintain. Even the parts that require modification and special behavior can be reused in your next project, thanks to the way controller endpoints and services are dynamically loaded into your Web API, without any dependencies.

If you work for a consulting company, you probably start several new projects each year with similar types of requirements, where you need to solve the commonalities for each new project. With an understanding of a client’s requirements and some initial implementation, a super-DRY approach enables you to literally finish an entire project in seconds. And of course, the composition of elements in your projects can be further reused, by identifying common modules, such as Authentication and Authorization. By implementing these modules in a common Web API project, you can apply them to any new project that poses similar problems to those you’ve seen before.

For the record, I’m making this sound easy, but the fact is that avoiding repetition is hard. It requires a willingness to refactor, refactor, refactor. And when you’re done refactoring, you need to refactor a bit more. But the upside is too good to ignore. DRY principles can let you almost magically create code, by simply waving your scaffolding wand and composing modules out of pre-existing parts.

At the end of the day, the principles articulated here can help you leverage existing best practices to create your own Web APIs while avoiding repetition. There’s a lot of good that can come from this approach, and hopefully it helps you appreciate the awesomeness of DRY.


Thomas Hansen is a Zen software wizard currently living in Cyprus, where he juggles software code working its way through FinTech and trading systems.

Thanks to the following Microsoft technical expert for reviewing this article: James McCaffrey


Discuss this article in the MSDN Magazine forum