Skip to content

Latest commit

 

History

History
520 lines (356 loc) · 14.3 KB

Slides.md

File metadata and controls

520 lines (356 loc) · 14.3 KB
theme _class paginate backgroundColor backgroundImage footer headingDivider marp style
gaia
lead
true
![width:300px](images/banner-transparent.png)
1
true
section { font-size: 25px; } container { height: 300px; width: 100%; display: block; justify-content: right; text-align: right; } header { float: right; } a { color: blue; text-decoration: underline; background-color: lightgrey; font-size: 80%; } table { font-size: 22px; }

BDSA: Session 7

Onion Architecture, Testing, and Advanced Database Schemas

Adrian Hoff Postdoctoral Researcher ITU

Todays's lecture

   

🧅 Onion architecture

🧪 Testing: EF Core & in-memory databases

🔗 Influencing the database schema in EF Core

Reflection on Chirp: What about architecture?

We have distinct components:

  • a domain model (currently the classes Cheep and Author)
  • repositories (e.g., ICheepRepository + CheepRepository)
  • services (e.g., ICheepService + CheepService)
  • a view / user interface (the Razor pages)
  • testing infrastructure  

Think about a good design for your app:

What component should depend on what other?

Onion Architecture

Onion Architecture is based on the inversion of control principle. Onion Architecture is comprised of multiple concentric layers interfacing each other towards the core that represents the domain.

Text and Image taken from: Tapas Pal Understanding Onion Architecture
  • The domain layer has no external dependencies while the individual layers are loosly coupled.
    • Replacing layer implementations is easy
    • Good testability: we can easily replace and mock layers in tests, e.g., database

bg right:45% 100%

Onion Architecture

How could we implement the onion architecture in Chirp?

Doodle a simple boxes-and-arrows diagram using a pen and paper

  • Boxes = .NET C# projects & namespaces
    • As projects use: Core, Infrastructure, Web, Tests
    • Fill the boxes with namespaces and classes, e.g., Chirp.Repositories, Program.cs, etc.
  • Arrows = Project dependencies
    • Remember dependency injection

bg right:45% 100%


bg center w:100% h:100%

  • This is one of many ways to implement the onion architecture
  • Beware: A multi-project setup requires adaptions to the
    commands for working with EF Core database migrations

Onion Architecture: Why is this good?

  • Loosely coupled and more independant layers!

  • How?

    • Inversion of Control: Dependency Injection
  • So what?

    • We can easily replace components!
      • Maintainability
      • Testability
      • ...

Dependency Injection (revisited)

Program.cs:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IChatService, ChatService>();
builder.Services.AddScoped<IMessageRepository, MessageRepository>();

ChatService.cs:

public class ChatService : IChatService
{
    private readonly IMessageRepository _messageRepository;
    public ChatService(IMessageRepository messageRepository)
    {
        _messageRepository = messageRepository;
    }

    ...
}

(These snippets should look familiar!)

Testing: loose component coupling helps

For example, we can replace a component by registering a test double - without further adaptions to the unit under test.

ChatServiceUnitTests.cs:

[Fact]
public void SomeUnitTestOnChatService()
{
    // Arrange
    IChatRepository chatRepo = new ChatRepositoryStub(...); // not the repository class used in production!
    IChatService service = new ChatService(chatRepo);

    // Act
    service.CreateMessage(...);

    // Assert
    ...
}

Testing: loose component coupling helps (ctd.)

Similarly, we can replace the real database with a fake in-memory database. This can be helpful when your database is slow or limits querying over time (e.g., in a cloud).

MessageRepositoryUnitTests.cs:

// Arrange
using var connection = new SqliteConnection("Filename=:memory:");
await connection.OpenAsync();
var builder = new DbContextOptionsBuilder<ChirpContext>().UseSqlite(connection);

using var context = new ChirpContext(builder.Options);
await context.Database.EnsureCreatedAsync(); // Applies the schema to the database

IMessageRepository repository = new MessageRepository(context);

// Act
var result = repository.QueryMessages("TestUser");
...
(Note: In the project, we use SQLite as our database in production! Nevertheless, using it in in-memory mode for testing is useful.)

Testing (ctd.)

Various parts of your system can be replaced with test doubles to test a targeted unit in isolation:

  • services
  • repositories
  • database contexts
  • databases
  • ...

bg right:65% 90%

Command Query Separation (CQS)

bg right:45% 100%

[CQS] states that every method should either be a command that performs an action, or a query that returns data to the caller, but not both.

Source: Wikipedia

If asking a question changes the answer, we're likely to run into problems.

Cf. Bertrand Meyer, Eiffel: a language for software engineering

Command Query Separation (CQS)

Revisit your project code

Do not change your code. Just think about the following points:  

  • Did you separate commands from queries in your repository?  
  • What makes mixing commands and queries tempting in your current setup?
    • How could we address this?

Unit of Work

The repository and unit of work patterns are intended to create an abstraction layer between the data access layer and the business logic layer of an application. Implementing these patterns can help insulate your application from changes in the data store and can facilitate automated unit testing [..]

Text and Image Source: Microsoft

bg right:59% 100%

Unit of Work Pattern (ctd.)

A Unit of Work bundles repositories and operations.  

  • Reliably and efficiently execute a batch of queries and/or commands
    • for instance: query from one repository, process the results, and send update commands to another repository  
  • Using EF Core, we should
    • first perform all data changes in memory, e.g., dbContext.Messages.Add(...)
    • and, if done successfully, persist them in our database via dbContext.SaveChanges()
  • Note: In a unit of work operation, use one EF Core database context per database

From domain model to data model

Remember last session?

  • We defined a domain model and used at as input for EF Core
  • EF Core mapped it to a database schema

  • Inspect XyzDBContextModelSnapshot.cs in your Migrations folder

    • Inspect how the BuildModel method defines your db schema

bg right:25% width:100%

From domain model to data model (ctd.)

The mapping process in EF Core is controlled largely by conventions (e.g., properties ending on Id are turned into keys).  

  • We can control this process further through

    • Data Annotations in the domain model

    • Fluent API: overriding the OnModelCreating(...) method in the database context class  

  • Precedence:   Fluent API   overrides   Data Annotations   overrides   Conventions

  Read more about this in the documentation on EF Core Modeling and, therein, Entity Properties.

Required properties

If you want to make sure that a property in your domain model is not null after creation, you can use the required modifier.

public required string MyProperty { get; set; }

However, EF Core does not respect the required modifier when creating a db schema. You will need to use the [Required] annotation from System.ComponentModel.DataAnnotations.

[Required]
public string MyProperty { get; set; }

String length limits

To validate the maximum length of strings, you can annotate a property directly:

[StringLength(500)]
public string Text { get; set; }

Alternatively, you can add constraings in the OnModelCreating method.

modelBuilder.Entity<Message>().Property(m => m.Text).HasMaxLength(500);

Try it out!

Recommended: Make sure you have committed all changes to your project

  • Running git status returns something like nothing to commit, working tree clean.

1. Limit the length of cheeps:

[Required]
[StringLength(500)]
public required string Text { get; set; }

2. Add a new migration: dotnet ef migrations add LimitAuthorInfoStringLength

3. Inspect the changes in XyzDBContextModelSnapshot.cs

  • Navigate to the Migrations folder in your console and and run git diff .

Unique properties

If you want to make sure that a property is unique, you can use the Index attribute:

[Index(IsUnique = true)]
public string MyProperty { get; set; }

Alternatively, add it in the OnModelCreating method:

modelBuilder.Entity<Author>()
    .HasIndex(c => c.Name)
    .IsUnique();

Hint: The fluent API applies a configuration in the order of method calls. If configurations are conflicting, the later method call will overrides the earlier call.

Try it out!

1. Make authors' names and emails unique

Add an OnModelCreating method to you database context:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<Author>()
        .HasIndex(c => c.Name)
        .IsUnique();
    modelBuilder.Entity<Author>()
        .HasIndex(c => c.Email)
        .IsUnique();
}

2. Add a new migration: dotnet ef migrations add UniqueAuthorNamesAndEmails

3. Inspect the changes in XyzDBContextModelSnapshot.cs (run git diff .)

Composite Primary Key

If you want to define a composite primary key (more than one column as the key), use the OnModelCreating method:

modelBuilder.Entity<Author>()
    .HasKey(k => new { k.FollowerId, k.FollowingId });

              Read more in the documentation on EF Core Modeling and, therein, Entity Properties.

Summary

  • 🧅 Onion architecture

    • helps with achieving loosely coupled components
    • domain model separated from rest of system and free of dependencies
    • individual components can be replaced more easily (e.g., for testing purposes)
  • 🧪 Testing: EF Core & in-memory databases

    • to test units in isolation, it is userful to replace collaborators with test doubles
      • includes the database, where using in-memory SQLite is a popular approach
      • it is easy to set up and fast in execution
  • 🔗 Influencing the database schema in EF Core

    • solved via data annotations and/or the fluent API (OnModelCreating method)
    • influences the database schema and, respectively, the migrations generated by EF Core

What to do now?

w:400px

Now: Guest Lecture

bg right

Martin von Haller Grønbæk, one of Denmark's leading IT lawyers, will give a guest lecture on Software Licenses and Software License Compatibility.

The lecture will start at 11:00 (sharp) in Auditorium 1. So be there on time, best 10:50. After his guest lecture, Martin will be available for deeper questions and discussions in the canteen (12:00 - 12:30).