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;
}
|
Adrian Hoff Postdoctoral Researcher ITU
We have distinct components:
- a domain model (currently the classes
Cheep
andAuthor
) - repositories (e.g.,
ICheepRepository
+CheepRepository
) - services (e.g.,
ICheepService
+CheepService
) - a view / user interface (the Razor pages)
- testing infrastructure
Text and Image taken from: Tapas Pal Understanding Onion ArchitectureOnion 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.
- 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
How could we implement the onion architecture in Chirp?
- 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.
- As projects use:
- Arrows = Project dependencies
- Remember dependency injection
- 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
-
- Inversion of Control: Dependency Injection
-
- We can easily replace components!
- Maintainability
- Testability
- ...
- We can easily replace components!
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!)
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
...
}
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");
...
Various parts of your system can be replaced with test doubles to test a targeted unit in isolation:
- services
- repositories
- database contexts
- databases
- ...
Microsoft, Choosing a testing strategy
[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
Cf. Bertrand Meyer, Eiffel: a language for software engineering
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?
Text and Image Source: MicrosoftThe 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 [..]
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()
- first perform all data changes in memory, e.g.,
- Note: In a unit of work operation, use one EF Core database context per database
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 how the
BuildModel
method defines your db schema
- Inspect how the
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.
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; }
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);
- Running
git status
returns something likenothing to commit, working tree clean
.
[Required]
[StringLength(500)]
public required string Text { get; set; }
- Navigate to the Migrations folder in your console and and run
git diff .
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.
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();
}
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.
-
- 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)
-
- 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
- to test units in isolation, it is userful to replace collaborators with test doubles
-
- solved via data annotations and/or the fluent API (
OnModelCreating
method) - influences the database schema and, respectively, the migrations generated by EF Core
- solved via data annotations and/or the fluent API (
-
If not done, complete the Tasks (blue slides) from this class
-
Check the reading material
-
Work on the project
-
If you feel you want prepare for next session, read chapters 6, 7, and 23 Andrew Lock ASP.NET Core in Action, Third Edition
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).