Click here to Skip to main content
15,860,859 members
Articles / Web Development / ASP.NET / ASP.NET Core

How to Organize Clean Architecture to Modular Patterns in 10 Minutes

Rate me:
Please Sign up or sign in to vote.
5.00/5 (30 votes)
22 Oct 2017CPOL10 min read 73.9K   77   8
Learn to organize clean architecture to modular patterns
You're still concerned about what is the modern software architecture at the moment? You're searching around for the best software architecture for applying to your project. Dive deep into this article, it's going to help you answer by yourself.

Introduction

In term of software development, the architecture of project is really important for the sake of maintenance and re-usability during many projects that I have worked. The software architecture ensures that the software that you build has a basis of the skeleton. It just likes the mainframe or the background of a house. Upon that, we have a freedom to build anything we want.

The question that keeps coming to me these days is how can we combine Clean Architecture and Modular pattern? I have done some of the experiments in the code, and finally, I decided to write it out in this article. The purpose is really straightforward for sharing what I have learned, and learn more what I get from feedbacks. This article is based on my experience in software development and how the modular approach gives the benefits and applies Clean Architecture concept on it.

Background

Modular Patterns

I am not going to talk too much about the theory because I want to focus on the implementation of it. What I want to say is all about the experience that I have work on this kind of architecture and patterns. The modular approach for the architecture is also a topic that we have talked a lot in our company. Back to 5 years ago, I worked on the big project that has a lot of people involved, and in that time, we had organized the architecture to the modular approach. And by the time, we know that with the modular, we can cut the large monolith software into many vertical smaller monoliths and make the team works more easily because each team only needs to focus on the module that they work. Could anyone remember about the conflicted code in the big project? Could you spend the half day (or more) just to merge the codes? It is really a nightmare, isn’t it?

So in the modular approach, we need to make sure that the modules should be independent enough so that it can be worked by the sole developers in different teams. It should be in logical design style and gives us strong points as:

  • Help our software system is able to extensible, reusable, maintainable, and adaptable.
  • Break large monolith stack into a flexible composite of collaborating modules (in monolith style).
  • Help newcomers to easily understand the business features and the functionalities of the system (because it is small enough)
  • Open the door to migrate into the microservices architecture (if needed, but it is not easy to adopt it from my point of view)

Clean Architecture

The Clean Architecture has been coined since 2012 by Uncle Bob, and by the time, it becomes important things in the software architecture world. We can see Android architecture using it by combination with MVP pattern to build the software architecture for the mobile app. A couple of articles out there also proposed to use Clean Architecture for the web application. Early this year, Uncle Bob published the book named “Clean Architecture: A Craftsman’s Guide to Software Structure and Design”. If you have never read that book before, I highly recommend you to take a look at it. This book mentions a lot of best practices when using SOLID principles, design patterns and some of tips and tricks in deployment work as well.

A little bit of overview about the clean architecture, if you have already known, please skip this and jump over to the implementation section. According to Clean Architecture, we need to ensure some of the important points below:

  • Independent of Frameworks. The architecture does not depend on the existence of some library of feature-laden software. This allows you to use such frameworks as tools, rather than having to cram your system into their limited constraints.
  • Testable. The business rules can be tested without the UI, Database, Web Server, or any other external element.
  • Independent of UI. The UI can change easily, without changing the rest of the system. A Web UI could be replaced with a console UI, for example, without changing the business rules.
  • Independent of Database. You can swap out Oracle or SQL Server, for Mongo, BigTable, CouchDB, or something else. Your business rules are not bound to the database.
  • Independent of any external agency. In fact, your business rules simply don’t know anything at all about the outside world.
Quote:

Items were copied from The Clean Architecture article by Uncle Bob. Thank you for the delightful clean architecture and patterns in this article.

How to Make It Work Perfectly?

Context is king, but a customer is more than the king. So let's start off with the main story today, any project has to analyze and ask the customer about what they want to do for their system and what they need for their system to act for. They will give us a bunch of use cases or user stories (if there is an Agile project). And the final step, we will have to draw the use case diagram. For those people that could not catch what I said so far, I would like to analyze an example in the blog engine domain. Let's say we want to build the blog website that has some of the features like read the blog, see the posts of this blog, add some of the comments in the public interface. And we definitely have a way to CRUD actions on blogs, posts, and comments. Besides that, we also need to login into the system before we can modify the blogs, posts, and comments. Based on the requirement of features for this application, we’re going to end up with the use case diagram below:

Image 1

The question is how can we make the architecture to the modular approach? Actually, the key factor is from Domain-Driven Design (Tackling Complexity in the Heart of Software and Implementing Domain-Driven Design books are good starting points from my point of view), and in this case, we use Bounded Context design pattern to analyse and design the current business domain. If you look at the diagram above, you will notice that we only have three main things for managing as authentication, blog, and post. I separate my application domain out to the Access Control Context, Blog Context and Post Context (I divide it into three Bounded Contexts because my domain knowledge is like that, it will be different from others due to different domain experts, but at least it needs to sort out the business needs). We end up with a diagram as:

Image 2

Let's explain a bit about the diagram above, the Access Control Context with the red color is for authentication and authorization tasks. The Blog Context filled with green color is for blog management includes setting up the blog, status assignment, and theme… The third one is the Post Context which manages its own comments and tags. As you can see, the Post Context has the relationship with others. For more information about how can we design Bounded Context (Root aggregate), please come to Effective Aggregate Design articles, you will learn a lot of useful things. You have my promise. :)

Due to the limitation of this article, I only can show you one Bounded Context code, and I would like to choose the Post Context because I think this is the one you would be most interested in. For the rest, you can take a look at my GitHub codes — the link given to you at the end of this article. So far, some of you will question what the heck Clean Architecture involves in this? Don’t panic. Let me show you the project structure, then you have the rest.

Image 3

Now I start to explain about the structure above. First, we have the Framework folder in the diagram, that contains all the stuff related to the necessary toolkit for the project. Remember that we’re going to avoid using the abstraction in our code, but use the composition (Composition over inheritance in OOP). The BlogCore.Core will not need to depend on any framework or library, but on .NET SDK definitely (some of the cases we call its vanilla code). Additionally, we have 3 projects: BlogCore.Infrastructure, BlogCore.Infrastructure.AspNetCore and BlogCore.Infrastructure.EfCore that will depend on EntityFrameworkCore, AspNetCore and others libraries like Autofac, AutoMapper, FluentValidation, MediatR

Second, Hosts folder in the middle of the diagram, We use it for putting the host projects there. You can see I have two hosts: one for the API (BlogCore.API) and another for the Single Page Application (BlogCore.App).

Third, Migrations folder is used to do the migrating works, in this case, we migrate data for the Access Control Context, Blog Context and Post Context. We can choose to migrate using Entity Framework migration and seed data for them. Otherwise, you can also use the T-SQL scripts to do the migration. It is up to you, I don’t say which one is better than others.

Finally, we have the Modules folder that contains the heart of this application. We have divided it into Bounded Context folders here, and make our architecture more clearly. Each Bounded Context has two sub-projects, e.g. BlogCore.PostContext and BlogCore.PostContext.Core. BlogCore.PostContext.Core only contains domain objects, contracts, and interfaces which is really good for another projects references. The rule is if we have another module that wants to use some of the classes, objects so that it should reference the <Module name>.Core and to the interface inside this project (only depends on the interface in another module. This will make loose-coupling for those modules. We will get a lot of benefits with this approach).

Let's see the Post Bounded Context structure in the details.

Image 4

I think we should deep dive into some of the code to understand more about how can we implement the Clean Architecture with the Modular pattern for this project.

We have Post.cs entity that acts like Root Aggregate in the Post Context as:

C#
namespace BlogCore.PostContext.Core.Domain
{
    public class Post : EntityBase
    {
        internal Post()
        {
        }

        internal Post(BlogId blogId, string title, 
                      string excerpt, string body, AuthorId authorId)
            : this(blogId, IdHelper.GenerateId(), title, excerpt, body, authorId)
        {
        }

        internal Post(BlogId blogId, Guid postId, string title, 
                      string excerpt, string body, AuthorId authorId) 
            : base(postId)
        {
            Blog = blogId;
            Title = title;
            Excerpt = excerpt;
            Slug = title.GenerateSlug();
            Body = body;
            Author = authorId;
            CreatedAt = DateTimeHelper.GenerateDateTime();
            Events.Add(new PostedCreated(postId));
        }

        public static Post CreateInstance(BlogId blogId, Guid postId, 
               string title, string excerpt, string body, AuthorId authorId)
        {
            return new Post(blogId, postId, title, excerpt, body, authorId);
        }

        public static Post CreateInstance(BlogId blogId, string title, 
               string excerpt, string body, AuthorId authorId)
        {
            return new Post(blogId, title, excerpt, body, authorId);
        }

        [Required]
        public string Title { get; private set; }

        [Required]
        public string Excerpt { get; private set; }

        [Required]
        public string Slug { get; private set; }

        [Required]
        public string Body { get; private set; }

        [Required]
        public BlogId Blog { get; private set; }

        public ICollection Comments { get; private set; } = new HashSet();

        public ICollection Tags { get; private set; } = new HashSet();

        [Required]
        public AuthorId Author { get; private set; }

        [Required]
        public DateTime CreatedAt { get; private set; }

        public DateTime UpdatedAt { get; private set; }

        public Post ChangeTitle(string title)
        {
            if (string.IsNullOrEmpty(title))
            {
                throw new BlogCore.Core.ValidationException
                                   ("Title could not be null or empty.");
            }

            Title = title;
            Slug = title.GenerateSlug();
            return this;
        }

        public Post ChangeExcerpt(string excerpt)
        {
            if (string.IsNullOrEmpty(excerpt))
            {
                throw new BlogCore.Core.ValidationException
                          ("Excerpt could not be null or empty.");
            }

            Excerpt = excerpt;
            return this;
        }

        public Post ChangeBody(string body)
        {
            if (string.IsNullOrEmpty(body))
            {
                throw new BlogCore.Core.ValidationException
                          ("Body could not be null or empty.");
            }

            Excerpt = body;
            return this;
        }

        public bool HasComments()
        {
            return Comments?.Any() ?? false;
        }

        public Post AddComment(string body, AuthorId authorId)
        {
            Comments.Add(new Comment(body, authorId));
            return this;
        }

        public Post UpdateComment(Guid commentId, string body)
        {
            var comment = Comments.FirstOrDefault(x => x.Id == commentId);
            if (comment == null)
            {
                throw new NotFoundCommentException
                ($"Could not find the comment with Id={commentId} for updating.");

            }
            comment.UpdateComment(body);
            return this;
        }

        public Post RemoveComment(Guid commentId)
        {
            var comment = Comments.FirstOrDefault(x => x.Id == commentId);
            if (comment == null)
            {
                throw new NotFoundCommentException
                ($"Could not find the comment with Id={commentId} for deleting.");
                
            }
            Comments.Remove(comment);
            return this;
        }

        public bool HasTags()
        {
            return Tags?.Any() ?? false;
        }

        public Post AssignTag(string name)
        {
            var tag = Tags.FirstOrDefault(x => x.Name == name);
            if (tag == null)
            {
                Tags.Add(new Tag(IdHelper.GenerateId(), name, 1));
            }
            else
            {
                tag.IncreaseFrequency();
            }
            return this;       
        }

        public Post RemoveTag(string name)
        {
            var tag = Tags.FirstOrDefault(x => x.Name == name);
            if (tag != null)
            {
                tag.DecreaseFrequency();
                Tags.Remove(tag);
            }
            return this;    
        }
    }
}

Then, we have the PostGenericRepository.cs:

C#
public class BlogEfRepository : EfRepository
        where TEntity : EntityBase
{
    public BlogEfRepository(PostDbContext dbContext)
        : base(dbContext)
    {
    }
}

We need to create the DbContext for the PostContext.cs as:

C#
namespace BlogCore.PostContext.Infrastructure
{
    public class PostDbContext : DbContext
    {
        public PostDbContext(DbContextOptions options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            var entityTypes = new List
            {
                typeof(Post),
                typeof(Comment),
                typeof(Tag)
            };

            var valueTypes = new List
            {
                typeof(BlogId),
                typeof(AuthorId)
            };

            base.OnModelCreating(modelBuilder.RegisterTypes
                                (entityTypes, valueTypes, "post", "post"));
        }
    }
}

In Clean Architecture, the use case is very important and should be designed very carefully. In my project, I named it as ListOutPostByBlogInteractor.cs:

C#
namespace BlogCore.PostContext.UseCases.ListOutPostByBlog
{
    public class ListOutPostByBlogInteractor 
        : IUseCaseRequestHandler<ListOutPostByBlogRequest, 
          PaginatedItem<ListOutPostByBlogResponse>>
    {
        private readonly IEfRepository<PostDbContext, Post> _postRepository;
        public IOptions<PagingOption> _pagingOption;

        public ListOutPostByBlogInteractor(
            IEfRepository<PostDbContext, Post> postRepository,
            IOptions<PagingOption> pagingOption)
        {
            _postRepository = postRepository;
            _pagingOption = pagingOption;
        }

        public IObservable<PaginatedItem<ListOutPostByBlogResponse>> 
               Process(ListOutPostByBlogRequest request)
        {
            var criterion = new Criterion
            (request.Page, _pagingOption.Value.PageSize, _pagingOption.Value);
            var includes = new Expression<Func<Post, object>>[] 
                { p => p.Comments, p => p.Author, p => p.Blog, p => p.Tags };
            Expression<Func<Post, bool>> filterFunc = x => x.Blog.Id == request.BlogId;

            return _postRepository.ListStream(filterFunc, criterion, includes)
                .Select(y =>
                {
                    return new PaginatedItem<ListOutPostByBlogResponse>(
                            y.TotalItems,
                            (int)y.TotalPages,
                            y.Items.Select(x =>
                            {
                                return new ListOutPostByBlogResponse(
                                    x.Id,
                                    x.Title,
                                    x.Excerpt,
                                    x.Slug,
                                    x.CreatedAt,
                                    new ListOutPostByBlogUserResponse(
                                            x.Author.Id.ToString(),
                                            string.Empty,
                                            string.Empty
                                        ),
                                    x.Tags.Select(
                                        tag => new ListOutPostByBlogTagResponse(
                                            tag.Id,
                                            tag.Name))
                                        .ToList()
                                );
                            }).ToList()
                        );
                });
        }
    }
}

After processing the business case for the Post Bounded Context, we need to aggregate some of the data related to the Access Control Bounded Context, and in this case, we get the Author information of it and use the IUserRepository interface to get the author’s details information. So that we introduce another class called ListOutPostByBlogPresenter.cs:

C#
namespace BlogCore.Api.Features.Posts.ListOutPostByBlog
{
    public class ListOutPostByBlogPresenter
    {
        private readonly IUserRepository _userRepository;

        public ListOutPostByBlogPresenter(IUserRepository userRepository)
        {
            _userRepository = userRepository;
        }

        public async Task<PaginatedItem<ListOutPostByBlogResponse>> 
        Transform(IObservable<PaginatedItem<ListOutPostByBlogResponse>> stream)
        {
            var result = await stream.Select(x => x);

            var authors = result.Items
                .Select(x => x.Author.Id)
                .Distinct()
                .Select(y => _userRepository.GetByIdAsync(y).Result)
                .ToList();

            var items = result.Items.Select(x =>
            {
                var author = authors.FirstOrDefault(au => au.Id == x.Author.Id.ToString());
                return x.SetAuthor(author?.Id, author?.FamilyName, author?.GivenName);
            });

            return new PaginatedItem<ListOutPostByBlogResponse>(
                result.TotalItems,
                (int)result.TotalPages,
                items.ToList());
        }
    }
}

And we need to have a place to register these dependency objects. So the Dependency Injection comes into play, and we use Autofac module in this project. The idea is the module will register all the dependency its own.

C#
namespace BlogCore.PostContext
{
    public class PostUseCaseModule : Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            base.Load(builder);

            builder.Register(x =>
                DbContextHelper.BuildDbContext<PostDbContext>(
                    x.ResolveKeyed<string>("MainDbConnectionString")))
                .SingleInstance();

            builder.RegisterType<ListOutPostByBlogInteractor>()
                .AsSelf()
                .SingleInstance();

            builder.RegisterType<ListOutPostByBlogPresenter>()
                .AsSelf()
                .SingleInstance();
        }
    }
}

Yes, that’s enough, then we only need to introduce the API for what we did:

C#
namespace BlogCore.PostContext
{
    [Produces("application/json")]
    [Route("public/api/blogs")]
    public class PostApiPublicController : Controller
    {
        private readonly ListOutPostByBlogInteractor _listOutPostByBlogInteractor;
        private readonly ListOutPostByBlogPresenter _listOutPostByBlogPresenter;

        public PostApiPublicController(
            ListOutPostByBlogInteractor listOutPostByBlogInteractor,
            ListOutPostByBlogPresenter listOutPostByBlogPresenter)
        {
            _listOutPostByBlogInteractor = listOutPostByBlogInteractor;
            _listOutPostByBlogPresenter = listOutPostByBlogPresenter;
        }

        [HttpGet("{blogId:guid}/posts")]
        public async Task> GetForBlog(Guid blogId, [FromQuery] int page)
        {
            var result = _listOutPostByBlogInteractor.Process
                         (new ListOutPostByBlogRequest(blogId, page <= 0 ? 1 : page));
            return await _listOutPostByBlogPresenter.Transform(result);
        }
    }
}

Recap

Today, I have walked you through the journey of how can we make the modular works well in the Clean Architecture. We at least know what is modular, what it’s actually important. We run through some of the overviews of Clean Architecture, and some of the strong points of it as well. And the last but not least, we know how to implement it using .NET Core 2.0.

Hope you guys can answer those questions in the headlines so far. What was not covered in this articles are the Data flow, Synchronized between Bounded Contexts, Unit Testing, Deployment for the Clean Architecture…I’m going to write more about them if you guys are interested in it, just drop a message in the comment box below.

Points of Interest

  • Knows how modular patterns can work with Clean Architecture in the same stack
  • Knows some of the pros & cons of Modular & Clean Architecture
  • Knows Clean Architecture in the practical view
  • Knows how to use the .NET Core 2.0 to implement the Blog Domain

Source Code

The source code of this article can be found at https://github.com/thangchung/blog-core.

History

  • 22nd October, 2017: Corrected typos and words to make the article easy to read and understand
  • 18th October, 2017: Updated the inline code because there are mistakes for generic types in it
  • 17th October, 2017: Initialized and wrote out the article completely

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect NashTech
Vietnam Vietnam
Thang has a great passion in .NET, JavaScript, and Node. You can visit Thang's blog at https://weblogs.asp.net/thangchung to read about his thoughts on software development.

Comments and Discussions

 
QuestionGreat article Pin
Ram0245520-Feb-18 4:57
Ram0245520-Feb-18 4:57 
QuestionVery good, one request. Pin
Dewey26-Oct-17 12:16
Dewey26-Oct-17 12:16 
QuestionDeeper Querys Pin
MarkusVT8823-Oct-17 21:58
professionalMarkusVT8823-Oct-17 21:58 
AnswerRe: Deeper Querys Pin
thangchung23-Oct-17 22:38
professionalthangchung23-Oct-17 22:38 
QuestionWinform application? Pin
Member 1019550523-Oct-17 10:15
professionalMember 1019550523-Oct-17 10:15 
AnswerRe: Winform application? Pin
thangchung23-Oct-17 18:04
professionalthangchung23-Oct-17 18:04 
PraiseA Good article Pin
Ming Xin19-Oct-17 6:07
professionalMing Xin19-Oct-17 6:07 
AnswerRe: A Good article Pin
thangchung19-Oct-17 17:15
professionalthangchung19-Oct-17 17:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.