CodeBlog.xyz

Suggested structure for folders (Onion Architecture)

August 30, 2023 | by Meir Achildiev

Suggested structure for folders (Onion Architecture)

Here’s a suggested structure for your folders (each folder would typically correspond to a project in the .NET solution):

  1. Core/Domain (Innermost Layer)
    • Entities
    • Interfaces
    • Enums
    • Exceptions
    • ValueObjects
  2. Application
    • Interfaces
    • Services
    • DTOs (Data Transfer Objects)
    • Mappers
    • Validators
  3. Infrastructure (Outer Layer)
    • Data
      • Configurations
      • Migrations
    • Repositories
    • Services
    • InfrastructureExtensions
  4. Presentation/UI
    • Controllers
    • Views
    • Filters
    • ViewModels
  5. Tests
    • UnitTests
    • IntegrationTests

Keep in mind that these are just suggestions, and the actual folder structure could vary based on your specific needs.

Here’s how to organize your code in a Visual Studio .NET Core project:

  1. Make Use of Folders: Visual Studio offers a Folder solution item that can be used to group similar files together, making your solution explorer less cluttered and easier to navigate.
  2. Namespace Management: Namespaces should mirror the folder hierarchy. This will make it easier to locate and identify where a specific class is located.
  3. Proper Naming Conventions: Adopt the .NET recommended naming conventions. For example, camelCase for local variables and method parameters, PascalCase for method, class, and property names, and so on.
  4. Region Tags: Region tags can be used to organize your code within a file, but should be used sparingly as they can make a file harder to read if overused.
  5. Solutions and Projects: Large applications should be broken down into multiple projects and possibly multiple solutions.
  6. Commenting and Documentation: Good comments and documentation are vital in understanding the code and its purpose. XML comments can be particularly useful as they can be used by IntelliSense and for generating documentation.
  7. Consistent Coding Style: Keeping a consistent coding style across the entire project is essential. You can use tools like EditorConfig or .NET Format to enforce consistent coding style rules.

Remember, the ultimate goal is to increase maintainability, readability, and scalability of your codebase. Thus, the organization should always cater to these principles.

About Services

In the context of Onion Architecture, “services” can mean different things based on where they’re located:

  1. Domain Services: These are part of your domain layer and contain business logic that doesn’t naturally fit into an entity or value object.
  2. Application Services: These are part of your application layer and act as the entry point to your application. They orchestrate tasks, delegate work to domain entities and domain services, and coordinate responses.
  3. Infrastructure Services: These are part of your infrastructure layer and typically interact with external resources such as databases, file systems, network, third-party services, etc.

The implementation of these services will be in the corresponding layers. For example, domain services will be implemented in the domain layer, application services in the application layer, and infrastructure services in the infrastructure layer. They communicate using interfaces (defined in the domain or application layer) and dependency injection, ensuring each layer is isolated and can be developed, tested, and modified independently.

For example, an EmailService (which sends emails) would be in the Infrastructure layer, as it relies on an external email server. It could implement an IEmailService interface, which is defined in the Application layer. The Application layer doesn’t need to know anything about how emails are sent (SMTP, API, etc.) – it just knows that it can use the IEmailService to send emails. This keeps your Application layer independent of specific infrastructure concerns.

The Application layer might contain a UserService with a method called CreateUser. This method would take in a DTO, validate it, create a User entity, use a repository to save it to the database, and use the IEmailService to send a welcome email. The UserService doesn’t know anything about databases or emails – it just coordinates the high-level process of creating a user. This keeps your application services focused on coordinating tasks and delegating work to other parts of the system.

Here’s a simplified example of a service that creates a User, including layers: Domain, Application, and Infrastructure. I’ll be using C# for the example.

We’ll use a simple User entity, a UserRepository, and a UserService.

Let’s start with the innermost layer, the Domain:

Domain/Core

public class User
{
    public Guid Id { get; private set; }
    public string Email { get; private set; }
    public string Name { get; private set; }

    private User() { }

    public User(string email, string name)
    {
        if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(name))
            throw new ArgumentException("Email and Name cannot be null or empty.");

        Id = Guid.NewGuid();
        Email = email;
        Name = name;
    }
}

public interface IUserRepository
{
    Task AddAsync(User user);
    Task<User> GetByEmailAsync(string email);
}

In the Application layer, we create the UserService:

Application

public class UserService
{
    private readonly IUserRepository _userRepository;

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

    public async Task CreateUserAsync(string email, string name)
    {
        var existingUser = await _userRepository.GetByEmailAsync(email);

        if (existingUser != null)
            throw new ArgumentException("User with the same email already exists.");

        var user = new User(email, name);

        await _userRepository.AddAsync(user);
    }
}

In the Infrastructure layer, we create the UserRepository:

Infrastructure

public class UserRepository : IUserRepository
{
    private readonly MyDbContext _context;

    public UserRepository(MyDbContext context)
    {
        _context = context;
    }

    public async Task AddAsync(User user)
    {
        await _context.Users.AddAsync(user);
        await _context.SaveChangesAsync();
    }

    public async Task<User> GetByEmailAsync(string email)
    {
        return await _context.Users.FirstOrDefaultAsync(u => u.Email == email);
    }
}

Here, MyDbContext would be your Entity Framework Core DbContext, and it would be configured with the appropriate database provider and connection string in your startup configuration.

Note: This is a basic example and doesn’t include considerations like exception handling, logging, and so on. It’s also assuming you’re directly using the User entity for creating users, but you might want to use a DTO (Data Transfer Object) in your service method and map it to a User entity inside the method.

Remember that this is just one way to organize your code with Onion Architecture. There are other practices that could be incorporated depending on the complexity of your project, such as separating your interfaces into an entirely separate project, or having separate read and write models in a CQRS pattern, etc. The most important part is maintaining the Dependency Rule: inner layers should never depend on outer layers.

RELATED POSTS

View all

view all