├── .gitattributes ├── .gitignore ├── Book.sln ├── Book.sln.DotSettings ├── Book ├── Book.csproj ├── Chapter10 │ ├── EF │ │ └── EF.cs │ ├── Migrations │ │ └── Migrations.cs │ ├── Transaction │ │ └── Transaction.cs │ └── V1 │ │ └── V1.cs ├── Chapter11 │ ├── CodePollution │ │ └── CodePollution.cs │ ├── LeakingKnowledge │ │ └── LeakingKnowledge.cs │ ├── MockingClasses │ │ └── MockingClasses.cs │ ├── PrivateMethods │ │ └── PrivateMethods.cs │ ├── PrivateState │ │ └── PrivateState.cs │ └── Time │ │ ├── Ambient.cs │ │ └── DI.cs ├── Chapter2 │ ├── Listing1 │ │ ├── CustomerTests.cs │ │ └── Other.cs │ └── Listing2 │ │ ├── CustomerTests.cs │ │ └── Other.cs ├── Chapter3 │ ├── CustomerTests_3 │ │ └── CustomerTests.cs │ ├── CustomerTests_4 │ │ └── CustomerTests.cs │ ├── FluentAssertions_1 │ │ └── CalculatorTests.cs │ ├── Listing1 │ │ └── CalculatorTests.cs │ └── Listing6 │ │ └── DeliveryServiceTests.cs ├── Chapter4 │ └── Listing1 │ │ └── MessageRendererTests.cs ├── Chapter5 │ ├── Listing1 │ │ └── Listing1.cs │ ├── Listing5 │ │ └── Listing5.cs │ ├── Listing6 │ │ └── Listing6.cs │ └── Listing9 │ │ └── CustomerControllerTests.cs ├── Chapter6 │ ├── Listing1 │ │ └── PriceEngine.cs │ ├── Listing2 │ │ └── PriceEngine.cs │ ├── Listing4_6 │ │ └── Listing4.cs │ └── Listing7_ │ │ ├── Before │ │ └── ArchitectureBefore.cs │ │ ├── Functional │ │ └── ArchitectureFunctional.cs │ │ └── Mocks │ │ └── ArchitectureMocks.cs ├── Chapter7 │ ├── CanExecute │ │ └── CanExecute.cs │ ├── DomainEvents │ │ └── DomainEvents.cs │ ├── Refactored_1 │ │ └── Refactored_1.cs │ ├── Refactored_2 │ │ └── Refactored_2.cs │ ├── Refactored_3 │ │ └── Refactored_3.cs │ └── SampleProject │ │ └── SampleProject.cs ├── Chapter8 │ ├── Circular │ │ └── Circular.cs │ ├── DI │ │ └── DI.cs │ ├── Logging │ │ └── V1.cs │ ├── LoggingV2 │ │ └── LoggingV2.cs │ ├── NonCircular │ │ └── NonCircular.cs │ └── Version1 │ │ └── V1.cs └── Chapter9 │ ├── V1 │ └── V1.cs │ └── V2 │ └── V2.cs ├── DatabaseGenerationScript.sql ├── LICENSE └── ReadMe.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Out 2 | .svn 3 | obj 4 | bin 5 | BenchmarkDotNet.Artifacts 6 | .idea 7 | _ReSharper* 8 | *.sln.GhostDoc.xml 9 | *.dotCover 10 | *.suo 11 | *.user 12 | *.Cache 13 | *.cache 14 | *.ncrunchsolution 15 | *.ncrunchproject 16 | /packages/*/* 17 | /Tools/*/* 18 | /.vs 19 | *ncrunch* -------------------------------------------------------------------------------- /Book.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2036 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Book", "Book\Book.csproj", "{EF19B3B2-F90C-4585-A186-59F14C8FFB2B}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{AD651139-2332-4511-AD53-9E544DAFEA9D}" 9 | ProjectSection(SolutionItems) = preProject 10 | DatabaseGenerationScript.sql = DatabaseGenerationScript.sql 11 | ReadMe.txt = ReadMe.txt 12 | EndProjectSection 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {EF19B3B2-F90C-4585-A186-59F14C8FFB2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {EF19B3B2-F90C-4585-A186-59F14C8FFB2B}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {EF19B3B2-F90C-4585-A186-59F14C8FFB2B}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {EF19B3B2-F90C-4585-A186-59F14C8FFB2B}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {53573788-4585-441A-B612-D0E2598D8F4C} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /Book/Book.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net47 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Book/Chapter10/EF/EF.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Data.SqlClient; 5 | using System.Linq; 6 | using System.Transactions; 7 | using Microsoft.EntityFrameworkCore; 8 | using Moq; 9 | using Xunit; 10 | 11 | namespace Book.Chapter10.EF 12 | { 13 | public class CrmContext : DbContext 14 | { 15 | public CrmContext(DbContextOptions options) 16 | : base(options) 17 | { 18 | } 19 | 20 | public CrmContext(string connectionString) 21 | : base (new DbContextOptionsBuilder().UseSqlServer(connectionString).Options) 22 | { 23 | } 24 | 25 | public DbSet Users { get; set; } 26 | public DbSet Companies { get; set; } 27 | 28 | protected override void OnModelCreating(ModelBuilder modelBuilder) 29 | { 30 | modelBuilder.Entity(x => 31 | { 32 | x.ToTable("User").HasKey(k => k.UserId); 33 | x.Property(k => k.Email); 34 | x.Property(k => k.Type); 35 | x.Property(k => k.IsEmailConfirmed); 36 | x.Ignore(k => k.DomainEvents); 37 | }); 38 | 39 | modelBuilder.Entity(x => 40 | { 41 | x.ToTable("Company").HasKey(k => k.DomainName); 42 | x.Property(p => p.DomainName); 43 | x.Property(p => p.NumberOfEmployees); 44 | }); 45 | } 46 | } 47 | 48 | public class User 49 | { 50 | public int UserId { get; set; } 51 | public string Email { get; private set; } 52 | public UserType Type { get; private set; } 53 | public bool IsEmailConfirmed { get; } 54 | public List DomainEvents { get; } 55 | 56 | public User(int userId, string email, UserType type, bool isEmailConfirmed) 57 | { 58 | UserId = userId; 59 | Email = email; 60 | Type = type; 61 | IsEmailConfirmed = isEmailConfirmed; 62 | DomainEvents = new List(); 63 | } 64 | 65 | public string CanChangeEmail() 66 | { 67 | if (IsEmailConfirmed) 68 | return "Can't change email after it's confirmed"; 69 | 70 | return null; 71 | } 72 | 73 | public void ChangeEmail(string newEmail, Company company) 74 | { 75 | Precondition.Requires(CanChangeEmail() == null); 76 | 77 | if (Email == newEmail) 78 | return; 79 | 80 | UserType newType = company.IsEmailCorporate(newEmail) 81 | ? UserType.Employee 82 | : UserType.Customer; 83 | 84 | if (Type != newType) 85 | { 86 | int delta = newType == UserType.Employee ? 1 : -1; 87 | company.ChangeNumberOfEmployees(delta); 88 | AddDomainEvent(new UserTypeChangedEvent(UserId, Type, newType)); 89 | } 90 | 91 | Email = newEmail; 92 | Type = newType; 93 | AddDomainEvent(new EmailChangedEvent(UserId, newEmail)); 94 | } 95 | 96 | private void AddDomainEvent(IDomainEvent domainEvent) 97 | { 98 | DomainEvents.Add(domainEvent); 99 | } 100 | } 101 | 102 | public class UserController 103 | { 104 | private readonly CrmContext _context; 105 | private readonly UserRepository _userRepository; 106 | private readonly CompanyRepository _companyRepository; 107 | private readonly EventDispatcher _eventDispatcher; 108 | 109 | public UserController( 110 | CrmContext context, 111 | MessageBus messageBus, 112 | IDomainLogger domainLogger) 113 | { 114 | _context = context; 115 | _userRepository = new UserRepository(context); 116 | _companyRepository = new CompanyRepository(context); 117 | _eventDispatcher = new EventDispatcher( 118 | messageBus, domainLogger); 119 | } 120 | 121 | public string ChangeEmail(int userId, string newEmail) 122 | { 123 | User user = _userRepository.GetUserById(userId); 124 | 125 | string error = user.CanChangeEmail(); 126 | if (error != null) 127 | return error; 128 | 129 | Company company = _companyRepository.GetCompany(); 130 | 131 | user.ChangeEmail(newEmail, company); 132 | 133 | _companyRepository.SaveCompany(company); 134 | _userRepository.SaveUser(user); 135 | _eventDispatcher.Dispatch(user.DomainEvents); 136 | 137 | _context.SaveChanges(); 138 | return "OK"; 139 | } 140 | } 141 | 142 | public class EventDispatcher 143 | { 144 | private readonly MessageBus _messageBus; 145 | private readonly IDomainLogger _domainLogger; 146 | 147 | public EventDispatcher( 148 | MessageBus messageBus, 149 | IDomainLogger domainLogger) 150 | { 151 | _domainLogger = domainLogger; 152 | _messageBus = messageBus; 153 | } 154 | 155 | public void Dispatch(List events) 156 | { 157 | foreach (IDomainEvent ev in events) 158 | { 159 | Dispatch(ev); 160 | } 161 | } 162 | 163 | private void Dispatch(IDomainEvent ev) 164 | { 165 | switch (ev) 166 | { 167 | case EmailChangedEvent emailChangedEvent: 168 | _messageBus.SendEmailChangedMessage( 169 | emailChangedEvent.UserId, 170 | emailChangedEvent.NewEmail); 171 | break; 172 | 173 | case UserTypeChangedEvent userTypeChangedEvent: 174 | _domainLogger.UserTypeHasChanged( 175 | userTypeChangedEvent.UserId, 176 | userTypeChangedEvent.OldType, 177 | userTypeChangedEvent.NewType); 178 | break; 179 | } 180 | } 181 | } 182 | 183 | public class UserFactory 184 | { 185 | public static User Create(object[] data) 186 | { 187 | Precondition.Requires(data.Length >= 3); 188 | 189 | int id = (int)data[0]; 190 | string email = (string)data[1]; 191 | UserType type = (UserType)data[2]; 192 | bool isEmailConfirmed = (bool)data[3]; 193 | 194 | return new User(id, email, type, isEmailConfirmed); 195 | } 196 | } 197 | 198 | public class CompanyFactory 199 | { 200 | public static Company Create(object[] data) 201 | { 202 | Precondition.Requires(data.Length >= 2); 203 | 204 | string domainName = (string)data[0]; 205 | int numberOfEmployees = (int)data[1]; 206 | 207 | return new Company(domainName, numberOfEmployees); 208 | } 209 | } 210 | 211 | public interface IDomainLogger 212 | { 213 | void UserTypeHasChanged(int userId, UserType oldType, UserType newType); 214 | } 215 | 216 | public class DomainLogger : IDomainLogger 217 | { 218 | private readonly ILogger _logger; 219 | 220 | public DomainLogger(ILogger logger) 221 | { 222 | _logger = logger; 223 | } 224 | 225 | public void UserTypeHasChanged( 226 | int userId, UserType oldType, UserType newType) 227 | { 228 | _logger.Info( 229 | $"User {userId} changed type " + 230 | $"from {oldType} to {newType}"); 231 | } 232 | } 233 | 234 | public interface ILogger 235 | { 236 | void Info(string s); 237 | } 238 | 239 | public class UserTypeChangedEvent : IDomainEvent 240 | { 241 | public int UserId { get; } 242 | public UserType OldType { get; } 243 | public UserType NewType { get; } 244 | 245 | public UserTypeChangedEvent(int userId, UserType oldType, UserType newType) 246 | { 247 | UserId = userId; 248 | OldType = oldType; 249 | NewType = newType; 250 | } 251 | 252 | protected bool Equals(UserTypeChangedEvent other) 253 | { 254 | return UserId == other.UserId && string.Equals(OldType, other.OldType); 255 | } 256 | 257 | public override bool Equals(object obj) 258 | { 259 | if (ReferenceEquals(null, obj)) 260 | { 261 | return false; 262 | } 263 | 264 | if (ReferenceEquals(this, obj)) 265 | { 266 | return true; 267 | } 268 | 269 | if (obj.GetType() != this.GetType()) 270 | { 271 | return false; 272 | } 273 | 274 | return Equals((EmailChangedEvent)obj); 275 | } 276 | 277 | public override int GetHashCode() 278 | { 279 | unchecked 280 | { 281 | return (UserId * 397) ^ OldType.GetHashCode(); 282 | } 283 | } 284 | } 285 | 286 | public class EmailChangedEvent : IDomainEvent 287 | { 288 | public int UserId { get; } 289 | public string NewEmail { get; } 290 | 291 | public EmailChangedEvent(int userId, string newEmail) 292 | { 293 | UserId = userId; 294 | NewEmail = newEmail; 295 | } 296 | 297 | protected bool Equals(EmailChangedEvent other) 298 | { 299 | return UserId == other.UserId && string.Equals(NewEmail, other.NewEmail); 300 | } 301 | 302 | public override bool Equals(object obj) 303 | { 304 | if (ReferenceEquals(null, obj)) 305 | { 306 | return false; 307 | } 308 | 309 | if (ReferenceEquals(this, obj)) 310 | { 311 | return true; 312 | } 313 | 314 | if (obj.GetType() != this.GetType()) 315 | { 316 | return false; 317 | } 318 | 319 | return Equals((EmailChangedEvent)obj); 320 | } 321 | 322 | public override int GetHashCode() 323 | { 324 | unchecked 325 | { 326 | return (UserId * 397) ^ (NewEmail != null ? NewEmail.GetHashCode() : 0); 327 | } 328 | } 329 | } 330 | 331 | public interface IDomainEvent 332 | { 333 | } 334 | 335 | public class Company 336 | { 337 | public string DomainName { get; } 338 | public int NumberOfEmployees { get; private set; } 339 | 340 | public Company(string domainName, int numberOfEmployees) 341 | { 342 | DomainName = domainName; 343 | NumberOfEmployees = numberOfEmployees; 344 | } 345 | 346 | public void ChangeNumberOfEmployees(int delta) 347 | { 348 | Precondition.Requires(NumberOfEmployees + delta >= 0); 349 | 350 | NumberOfEmployees += delta; 351 | } 352 | 353 | public bool IsEmailCorporate(string email) 354 | { 355 | string emailDomain = email.Split('@')[1]; 356 | return emailDomain == DomainName; 357 | } 358 | } 359 | 360 | public enum UserType 361 | { 362 | Customer = 1, 363 | Employee = 2 364 | } 365 | 366 | public static class Precondition 367 | { 368 | public static void Requires(bool precondition, string message = null) 369 | { 370 | if (precondition == false) 371 | throw new Exception(message); 372 | } 373 | } 374 | 375 | public class UserControllerTestsBad 376 | { 377 | private const string ConnectionString = @"Server=.\Sql;Database=IntegrationTests;Trusted_Connection=true;"; 378 | 379 | [Fact] 380 | public void Changing_email_from_corporate_to_non_corporate() 381 | { 382 | var optionsBuilder = new DbContextOptionsBuilder() 383 | .UseSqlServer(ConnectionString); 384 | 385 | using (var context = new CrmContext(optionsBuilder.Options)) 386 | { 387 | // Arrange 388 | var userRepository = new UserRepository(context); 389 | var companyRepository = new CompanyRepository(context); 390 | var user = new User(0, "user@mycorp.com", 391 | UserType.Employee, false); 392 | userRepository.SaveUser(user); 393 | var company = new Company("mycorp.com", 1); 394 | companyRepository.SaveCompany(company); 395 | context.SaveChanges(); 396 | 397 | var busSpy = new BusSpy(); 398 | var messageBus = new MessageBus(busSpy); 399 | var loggerMock = new Mock(); 400 | var sut = new UserController( 401 | context, messageBus, loggerMock.Object); 402 | 403 | // Act 404 | string result = sut.ChangeEmail(user.UserId, "new@gmail.com"); 405 | 406 | // Assert 407 | Assert.Equal("OK", result); 408 | 409 | User userFromDb = userRepository.GetUserById(user.UserId); 410 | Assert.Equal("new@gmail.com", userFromDb.Email); 411 | Assert.Equal(UserType.Customer, userFromDb.Type); 412 | 413 | Company companyFromDb = companyRepository.GetCompany(); 414 | Assert.Equal(0, companyFromDb.NumberOfEmployees); 415 | 416 | busSpy.ShouldSendNumberOfMessages(1) 417 | .WithEmailChangedMessage(user.UserId, "new@gmail.com"); 418 | loggerMock.Verify( 419 | x => x.UserTypeHasChanged( 420 | user.UserId, UserType.Employee, UserType.Customer), 421 | Times.Once); 422 | } 423 | } 424 | } 425 | 426 | public class UserControllerTests : IntegrationTests 427 | { 428 | [Fact] 429 | public void Changing_email_from_corporate_to_non_corporate() 430 | { 431 | // Arrange 432 | User user = CreateUser("user@mycorp.com", UserType.Employee); 433 | CreateCompany("mycorp.com", 1); 434 | 435 | var busSpy = new BusSpy(); 436 | var messageBus = new MessageBus(busSpy); 437 | var loggerMock = new Mock(); 438 | 439 | // Act 440 | string result = Execute( 441 | x => x.ChangeEmail(user.UserId, "new@gmail.com"), 442 | messageBus, loggerMock.Object); 443 | 444 | // Assert 445 | Assert.Equal("OK", result); 446 | 447 | User userFromDb = QueryUser(user.UserId); 448 | userFromDb 449 | .ShouldExist() 450 | .WithEmail("new@gmail.com") 451 | .WithType(UserType.Customer); 452 | Company companyFromDb = QueryCompany(); 453 | Assert.Equal(0, companyFromDb.NumberOfEmployees); 454 | 455 | busSpy.ShouldSendNumberOfMessages(1) 456 | .WithEmailChangedMessage(user.UserId, "new@gmail.com"); 457 | loggerMock.Verify( 458 | x => x.UserTypeHasChanged( 459 | user.UserId, UserType.Employee, UserType.Customer), 460 | Times.Once); 461 | } 462 | 463 | private string Execute(Func func, MessageBus messageBus, IDomainLogger logger) 464 | { 465 | using (var context = new CrmContext(ConnectionString)) 466 | { 467 | var controller = new UserController(context, messageBus, logger); 468 | return func(controller); 469 | } 470 | } 471 | 472 | private Company QueryCompany() 473 | { 474 | using (var context = new CrmContext(ConnectionString)) 475 | { 476 | var repository = new CompanyRepository(context); 477 | return repository.GetCompany(); 478 | } 479 | } 480 | 481 | private User QueryUser(int userId) 482 | { 483 | using (var context = new CrmContext(ConnectionString)) 484 | { 485 | var repository = new UserRepository(context); 486 | return repository.GetUserById(userId); 487 | } 488 | } 489 | 490 | private User CreateUser( 491 | string email = "user@mycorp.com", 492 | UserType type = UserType.Employee, 493 | bool isEmailConfirmed = false) 494 | { 495 | using (var context = new CrmContext(ConnectionString)) 496 | { 497 | var user = new User(0, email, type, isEmailConfirmed); 498 | var repository = new UserRepository(context); 499 | repository.SaveUser(user); 500 | 501 | context.SaveChanges(); 502 | 503 | return user; 504 | } 505 | } 506 | 507 | private Company CreateCompany(string domainName, int numberOfEmployees) 508 | { 509 | using (var context = new CrmContext(ConnectionString)) 510 | { 511 | var company = new Company(domainName, numberOfEmployees); 512 | var repository = new CompanyRepository(context); 513 | repository.AddCompany(company); 514 | 515 | context.SaveChanges(); 516 | 517 | return company; 518 | } 519 | } 520 | } 521 | 522 | public static class UserExternsions 523 | { 524 | public static User ShouldExist(this User user) 525 | { 526 | Assert.NotNull(user); 527 | return user; 528 | } 529 | 530 | public static User WithEmail(this User user, string email) 531 | { 532 | Assert.Equal(email, user.Email); 533 | return user; 534 | } 535 | 536 | public static User WithType(this User user, UserType type) 537 | { 538 | Assert.Equal(type, user.Type); 539 | return user; 540 | } 541 | } 542 | 543 | public abstract class IntegrationTests 544 | { 545 | protected const string ConnectionString = @"Server=.\Sql;Database=IntegrationTests;Trusted_Connection=true;"; 546 | 547 | protected IntegrationTests() 548 | { 549 | ClearDatabase(); 550 | } 551 | 552 | private void ClearDatabase() 553 | { 554 | string query = 555 | "DELETE FROM dbo.[User];" + 556 | "DELETE FROM dbo.Company;"; 557 | 558 | using (var connection = new SqlConnection(ConnectionString)) 559 | { 560 | var command = new SqlCommand(query, connection) 561 | { 562 | CommandType = CommandType.Text 563 | }; 564 | 565 | connection.Open(); 566 | command.ExecuteNonQuery(); 567 | } 568 | } 569 | } 570 | 571 | public class UserRepository 572 | { 573 | private readonly CrmContext _context; 574 | 575 | public UserRepository(CrmContext context) 576 | { 577 | _context = context; 578 | } 579 | 580 | public User GetUserById(int userId) 581 | { 582 | return _context.Users 583 | .SingleOrDefault(x => x.UserId == userId); 584 | } 585 | 586 | public void SaveUser(User user) 587 | { 588 | _context.Users.Update(user); 589 | } 590 | } 591 | 592 | public class CompanyRepository 593 | { 594 | private readonly CrmContext _context; 595 | 596 | public CompanyRepository(CrmContext context) 597 | { 598 | _context = context; 599 | } 600 | 601 | public Company GetCompany() 602 | { 603 | return _context.Companies 604 | .SingleOrDefault(); 605 | } 606 | 607 | public void SaveCompany(Company company) 608 | { 609 | _context.Companies.Update(company); 610 | } 611 | 612 | public void AddCompany(Company company) 613 | { 614 | _context.Companies.Add(company); 615 | } 616 | } 617 | 618 | public class Transaction : IDisposable 619 | { 620 | private readonly TransactionScope _transaction; 621 | public readonly string ConnectionString; 622 | 623 | public Transaction(string connectionString) 624 | { 625 | _transaction = new TransactionScope(); 626 | ConnectionString = connectionString; 627 | } 628 | 629 | public void Commit() 630 | { 631 | _transaction.Complete(); 632 | } 633 | 634 | public void Dispose() 635 | { 636 | _transaction?.Dispose(); 637 | } 638 | } 639 | 640 | public class MessageBus 641 | { 642 | private readonly IBus _bus; 643 | 644 | public MessageBus(IBus bus) 645 | { 646 | _bus = bus; 647 | } 648 | 649 | public void SendEmailChangedMessage(int userId, string newEmail) 650 | { 651 | _bus.Send("Type: USER EMAIL CHANGED; " + 652 | $"Id: {userId}; " + 653 | $"NewEmail: {newEmail}"); 654 | } 655 | } 656 | 657 | public interface IBus 658 | { 659 | void Send(string message); 660 | } 661 | 662 | public class BusSpy : IBus 663 | { 664 | private List _sentMessages = new List(); 665 | 666 | public void Send(string message) 667 | { 668 | _sentMessages.Add(message); 669 | } 670 | 671 | public BusSpy ShouldSendNumberOfMessages(int number) 672 | { 673 | Assert.Equal(number, _sentMessages.Count); 674 | return this; 675 | } 676 | 677 | public BusSpy WithEmailChangedMessage(int userId, string newEmail) 678 | { 679 | string message = "Type: USER EMAIL CHANGED; " + 680 | $"Id: {userId}; " + 681 | $"NewEmail: {newEmail}"; 682 | Assert.Contains(_sentMessages, x => x == message); 683 | 684 | return this; 685 | } 686 | } 687 | } 688 | -------------------------------------------------------------------------------- /Book/Chapter10/Migrations/Migrations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Book.Chapter10.Migrations 4 | { 5 | [Migration(1)] 6 | public class CreateUserTable : Migration 7 | { 8 | public override void Up() 9 | { 10 | Create.Table("Users"); 11 | } 12 | 13 | public override void Down() 14 | { 15 | Delete.Table("Users"); 16 | } 17 | } 18 | 19 | public class Delete 20 | { 21 | public static void Table(string users) 22 | { 23 | throw new NotImplementedException(); 24 | } 25 | } 26 | 27 | public class Create 28 | { 29 | public static void Table(string users) 30 | { 31 | throw new NotImplementedException(); 32 | } 33 | } 34 | 35 | public class Migration 36 | { 37 | public virtual void Up() 38 | { 39 | throw new NotImplementedException(); 40 | } 41 | 42 | public virtual void Down() 43 | { 44 | throw new NotImplementedException(); 45 | } 46 | } 47 | 48 | public class MigrationAttribute : Attribute 49 | { 50 | public MigrationAttribute(int i) 51 | { 52 | throw new NotImplementedException(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Book/Chapter10/Transaction/Transaction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.SqlClient; 4 | using System.Linq; 5 | using System.Transactions; 6 | using Dapper; 7 | using Moq; 8 | using Xunit; 9 | 10 | namespace Book.Chapter10.Transaction 11 | { 12 | public class User 13 | { 14 | public int UserId { get; set; } 15 | public string Email { get; private set; } 16 | public UserType Type { get; private set; } 17 | public bool IsEmailConfirmed { get; } 18 | public List DomainEvents { get; } 19 | 20 | public User(int userId, string email, UserType type, bool isEmailConfirmed) 21 | { 22 | UserId = userId; 23 | Email = email; 24 | Type = type; 25 | IsEmailConfirmed = isEmailConfirmed; 26 | DomainEvents = new List(); 27 | } 28 | 29 | public string CanChangeEmail() 30 | { 31 | if (IsEmailConfirmed) 32 | return "Can't change email after it's confirmed"; 33 | 34 | return null; 35 | } 36 | 37 | public void ChangeEmail(string newEmail, Company company) 38 | { 39 | Precondition.Requires(CanChangeEmail() == null); 40 | 41 | if (Email == newEmail) 42 | return; 43 | 44 | UserType newType = company.IsEmailCorporate(newEmail) 45 | ? UserType.Employee 46 | : UserType.Customer; 47 | 48 | if (Type != newType) 49 | { 50 | int delta = newType == UserType.Employee ? 1 : -1; 51 | company.ChangeNumberOfEmployees(delta); 52 | AddDomainEvent(new UserTypeChangedEvent(UserId, Type, newType)); 53 | } 54 | 55 | Email = newEmail; 56 | Type = newType; 57 | AddDomainEvent(new EmailChangedEvent(UserId, newEmail)); 58 | } 59 | 60 | private void AddDomainEvent(IDomainEvent domainEvent) 61 | { 62 | DomainEvents.Add(domainEvent); 63 | } 64 | } 65 | 66 | public class UserController 67 | { 68 | private readonly Transaction _transaction; 69 | private readonly UserRepository _userRepository; 70 | private readonly CompanyRepository _companyRepository; 71 | private readonly EventDispatcher _eventDispatcher; 72 | 73 | public UserController( 74 | Transaction transaction, 75 | MessageBus messageBus, 76 | IDomainLogger domainLogger) 77 | { 78 | _transaction = transaction; 79 | _userRepository = new UserRepository(transaction); 80 | _companyRepository = new CompanyRepository(transaction); 81 | _eventDispatcher = new EventDispatcher( 82 | messageBus, domainLogger); 83 | } 84 | 85 | public string ChangeEmail(int userId, string newEmail) 86 | { 87 | object[] userData = _userRepository.GetUserById(userId); 88 | User user = UserFactory.Create(userData); 89 | 90 | string error = user.CanChangeEmail(); 91 | if (error != null) 92 | return error; 93 | 94 | object[] companyData = _companyRepository.GetCompany(); 95 | Company company = CompanyFactory.Create(companyData); 96 | 97 | user.ChangeEmail(newEmail, company); 98 | 99 | _companyRepository.SaveCompany(company); 100 | _userRepository.SaveUser(user); 101 | _eventDispatcher.Dispatch(user.DomainEvents); 102 | 103 | _transaction.Commit(); 104 | return "OK"; 105 | } 106 | } 107 | 108 | public class EventDispatcher 109 | { 110 | private readonly MessageBus _messageBus; 111 | private readonly IDomainLogger _domainLogger; 112 | 113 | public EventDispatcher( 114 | MessageBus messageBus, 115 | IDomainLogger domainLogger) 116 | { 117 | _domainLogger = domainLogger; 118 | _messageBus = messageBus; 119 | } 120 | 121 | public void Dispatch(List events) 122 | { 123 | foreach (IDomainEvent ev in events) 124 | { 125 | Dispatch(ev); 126 | } 127 | } 128 | 129 | private void Dispatch(IDomainEvent ev) 130 | { 131 | switch (ev) 132 | { 133 | case EmailChangedEvent emailChangedEvent: 134 | _messageBus.SendEmailChangedMessage( 135 | emailChangedEvent.UserId, 136 | emailChangedEvent.NewEmail); 137 | break; 138 | 139 | case UserTypeChangedEvent userTypeChangedEvent: 140 | _domainLogger.UserTypeHasChanged( 141 | userTypeChangedEvent.UserId, 142 | userTypeChangedEvent.OldType, 143 | userTypeChangedEvent.NewType); 144 | break; 145 | } 146 | } 147 | } 148 | 149 | public class UserFactory 150 | { 151 | public static User Create(object[] data) 152 | { 153 | Precondition.Requires(data.Length >= 3); 154 | 155 | int id = (int)data[0]; 156 | string email = (string)data[1]; 157 | UserType type = (UserType)data[2]; 158 | bool isEmailConfirmed = (bool)data[3]; 159 | 160 | return new User(id, email, type, isEmailConfirmed); 161 | } 162 | } 163 | 164 | public class CompanyFactory 165 | { 166 | public static Company Create(object[] data) 167 | { 168 | Precondition.Requires(data.Length >= 2); 169 | 170 | string domainName = (string)data[0]; 171 | int numberOfEmployees = (int)data[1]; 172 | 173 | return new Company(domainName, numberOfEmployees); 174 | } 175 | } 176 | 177 | public interface IDomainLogger 178 | { 179 | void UserTypeHasChanged(int userId, UserType oldType, UserType newType); 180 | } 181 | 182 | public class DomainLogger : IDomainLogger 183 | { 184 | private readonly ILogger _logger; 185 | 186 | public DomainLogger(ILogger logger) 187 | { 188 | _logger = logger; 189 | } 190 | 191 | public void UserTypeHasChanged( 192 | int userId, UserType oldType, UserType newType) 193 | { 194 | _logger.Info( 195 | $"User {userId} changed type " + 196 | $"from {oldType} to {newType}"); 197 | } 198 | } 199 | 200 | public interface ILogger 201 | { 202 | void Info(string s); 203 | } 204 | 205 | public class UserTypeChangedEvent : IDomainEvent 206 | { 207 | public int UserId { get; } 208 | public UserType OldType { get; } 209 | public UserType NewType { get; } 210 | 211 | public UserTypeChangedEvent(int userId, UserType oldType, UserType newType) 212 | { 213 | UserId = userId; 214 | OldType = oldType; 215 | NewType = newType; 216 | } 217 | 218 | protected bool Equals(UserTypeChangedEvent other) 219 | { 220 | return UserId == other.UserId && string.Equals(OldType, other.OldType); 221 | } 222 | 223 | public override bool Equals(object obj) 224 | { 225 | if (ReferenceEquals(null, obj)) 226 | { 227 | return false; 228 | } 229 | 230 | if (ReferenceEquals(this, obj)) 231 | { 232 | return true; 233 | } 234 | 235 | if (obj.GetType() != this.GetType()) 236 | { 237 | return false; 238 | } 239 | 240 | return Equals((EmailChangedEvent)obj); 241 | } 242 | 243 | public override int GetHashCode() 244 | { 245 | unchecked 246 | { 247 | return (UserId * 397) ^ OldType.GetHashCode(); 248 | } 249 | } 250 | } 251 | 252 | public class EmailChangedEvent : IDomainEvent 253 | { 254 | public int UserId { get; } 255 | public string NewEmail { get; } 256 | 257 | public EmailChangedEvent(int userId, string newEmail) 258 | { 259 | UserId = userId; 260 | NewEmail = newEmail; 261 | } 262 | 263 | protected bool Equals(EmailChangedEvent other) 264 | { 265 | return UserId == other.UserId && string.Equals(NewEmail, other.NewEmail); 266 | } 267 | 268 | public override bool Equals(object obj) 269 | { 270 | if (ReferenceEquals(null, obj)) 271 | { 272 | return false; 273 | } 274 | 275 | if (ReferenceEquals(this, obj)) 276 | { 277 | return true; 278 | } 279 | 280 | if (obj.GetType() != this.GetType()) 281 | { 282 | return false; 283 | } 284 | 285 | return Equals((EmailChangedEvent)obj); 286 | } 287 | 288 | public override int GetHashCode() 289 | { 290 | unchecked 291 | { 292 | return (UserId * 397) ^ (NewEmail != null ? NewEmail.GetHashCode() : 0); 293 | } 294 | } 295 | } 296 | 297 | public interface IDomainEvent 298 | { 299 | } 300 | 301 | public class Company 302 | { 303 | public string DomainName { get; } 304 | public int NumberOfEmployees { get; private set; } 305 | 306 | public Company(string domainName, int numberOfEmployees) 307 | { 308 | DomainName = domainName; 309 | NumberOfEmployees = numberOfEmployees; 310 | } 311 | 312 | public void ChangeNumberOfEmployees(int delta) 313 | { 314 | Precondition.Requires(NumberOfEmployees + delta >= 0); 315 | 316 | NumberOfEmployees += delta; 317 | } 318 | 319 | public bool IsEmailCorporate(string email) 320 | { 321 | string emailDomain = email.Split('@')[1]; 322 | return emailDomain == DomainName; 323 | } 324 | } 325 | 326 | public enum UserType 327 | { 328 | Customer = 1, 329 | Employee = 2 330 | } 331 | 332 | public static class Precondition 333 | { 334 | public static void Requires(bool precondition, string message = null) 335 | { 336 | if (precondition == false) 337 | throw new Exception(message); 338 | } 339 | } 340 | 341 | public class UserControllerTests 342 | { 343 | private const string ConnectionString = @"Server=.\Sql;Database=IntegrationTests;Trusted_Connection=true;"; 344 | 345 | [Fact] 346 | public void Changing_email_from_corporate_to_non_corporate() 347 | { 348 | // Arrange 349 | User user; 350 | using (var transaction = new Transaction(ConnectionString)) 351 | { 352 | var userRepository = new UserRepository(transaction); 353 | var companyRepository = new CompanyRepository(transaction); 354 | user = CreateUser( 355 | "user@mycorp.com", UserType.Employee, userRepository); 356 | CreateCompany("mycorp.com", 1, companyRepository); 357 | 358 | transaction.Commit(); 359 | } 360 | 361 | var busSpy = new BusSpy(); 362 | var messageBus = new MessageBus(busSpy); 363 | var loggerMock = new Mock(); 364 | 365 | string result; 366 | using (var transaction = new Transaction(ConnectionString)) 367 | { 368 | var sut = new UserController(transaction, messageBus, loggerMock.Object); 369 | 370 | // Act 371 | result = sut.ChangeEmail(user.UserId, "new@gmail.com"); 372 | } 373 | 374 | // Assert 375 | Assert.Equal("OK", result); 376 | 377 | using (var transaction = new Transaction(ConnectionString)) 378 | { 379 | var userRepository = new UserRepository(transaction); 380 | var companyRepository = new CompanyRepository(transaction); 381 | 382 | object[] userData = userRepository.GetUserById(user.UserId); 383 | User userFromDb = UserFactory.Create(userData); 384 | Assert.Equal("new@gmail.com", userFromDb.Email); 385 | Assert.Equal(UserType.Customer, userFromDb.Type); 386 | 387 | object[] companyData = companyRepository.GetCompany(); 388 | Company companyFromDb = CompanyFactory.Create(companyData); 389 | Assert.Equal(0, companyFromDb.NumberOfEmployees); 390 | 391 | busSpy.ShouldSendNumberOfMessages(1) 392 | .WithEmailChangedMessage(user.UserId, "new@gmail.com"); 393 | loggerMock.Verify( 394 | x => x.UserTypeHasChanged( 395 | user.UserId, UserType.Employee, UserType.Customer), 396 | Times.Once); 397 | } 398 | } 399 | 400 | private Company CreateCompany(string domainName, int numberOfEmployees, CompanyRepository repository) 401 | { 402 | var company = new Company(domainName, numberOfEmployees); 403 | repository.SaveCompany(company); 404 | return company; 405 | } 406 | 407 | private User CreateUser(string email, UserType type, UserRepository repository) 408 | { 409 | var user = new User(0, email, type, false); 410 | repository.SaveUser(user); 411 | return user; 412 | } 413 | } 414 | 415 | public class UserRepository 416 | { 417 | private readonly Transaction _transaction; 418 | 419 | public UserRepository(Transaction transaction) 420 | { 421 | _transaction = transaction; 422 | } 423 | 424 | public object[] GetUserById(int userId) 425 | { 426 | using (SqlConnection connection = new SqlConnection(_transaction.ConnectionString)) 427 | { 428 | string query = "SELECT * FROM [dbo].[User] WHERE UserID = @UserID"; 429 | dynamic data = connection.QuerySingle(query, new { UserID = userId }); 430 | 431 | return new object[] 432 | { 433 | data.UserID, 434 | data.Email, 435 | data.Type, 436 | data.IsEmailConfirmed 437 | }; 438 | } 439 | } 440 | 441 | public void SaveUser(User user) 442 | { 443 | using (var connection = new SqlConnection(_transaction.ConnectionString)) 444 | { 445 | string updateQuery = @" 446 | UPDATE [dbo].[User] 447 | SET Email = @Email, Type = @Type, 448 | IsEmailConfirmed = @IsEmailConfirmed 449 | WHERE UserID = @UserID 450 | SELECT @UserID"; 451 | 452 | string insertQuery = @" 453 | INSERT [dbo].[User] (Email, Type, IsEmailConfirmed) 454 | VALUES (@Email, @Type, @IsEmailConfirmed) 455 | SELECT CAST(SCOPE_IDENTITY() as int)"; 456 | 457 | string query = user.UserId == 0 458 | ? insertQuery 459 | : updateQuery; 460 | int userId = connection.Query(query, new 461 | { 462 | user.Email, 463 | user.UserId, 464 | user.IsEmailConfirmed, 465 | Type = (int)user.Type 466 | }) 467 | .Single(); 468 | 469 | user.UserId = userId; 470 | } 471 | } 472 | } 473 | 474 | public class CompanyRepository 475 | { 476 | private readonly Transaction _transaction; 477 | 478 | public CompanyRepository(Transaction transaction) 479 | { 480 | _transaction = transaction; 481 | } 482 | 483 | public object[] GetCompany() 484 | { 485 | using (SqlConnection connection = new SqlConnection(_transaction.ConnectionString)) 486 | { 487 | string query = "SELECT * FROM dbo.Company"; 488 | dynamic data = connection.QuerySingle(query); 489 | 490 | return new object[] 491 | { 492 | data.DomainName, 493 | data.NumberOfEmployees 494 | }; 495 | } 496 | } 497 | 498 | public void SaveCompany(Company company) 499 | { 500 | using (var connection = new SqlConnection(_transaction.ConnectionString)) 501 | { 502 | string query = @" 503 | UPDATE dbo.Company 504 | SET DomainName = @DomainName, 505 | NumberOfEmployees = @NumberOfEmployees"; 506 | 507 | connection.Execute(query, new 508 | { 509 | company.DomainName, 510 | company.NumberOfEmployees 511 | }); 512 | } 513 | } 514 | } 515 | 516 | public class Transaction : IDisposable 517 | { 518 | private readonly TransactionScope _transaction; 519 | public readonly string ConnectionString; 520 | 521 | public Transaction(string connectionString) 522 | { 523 | _transaction = new TransactionScope(); 524 | ConnectionString = connectionString; 525 | } 526 | 527 | public void Commit() 528 | { 529 | _transaction.Complete(); 530 | } 531 | 532 | public void Dispose() 533 | { 534 | _transaction?.Dispose(); 535 | } 536 | } 537 | 538 | public class MessageBus 539 | { 540 | private readonly IBus _bus; 541 | 542 | public MessageBus(IBus bus) 543 | { 544 | _bus = bus; 545 | } 546 | 547 | public void SendEmailChangedMessage(int userId, string newEmail) 548 | { 549 | _bus.Send("Type: USER EMAIL CHANGED; " + 550 | $"Id: {userId}; " + 551 | $"NewEmail: {newEmail}"); 552 | } 553 | } 554 | 555 | public interface IBus 556 | { 557 | void Send(string message); 558 | } 559 | 560 | public class BusSpy : IBus 561 | { 562 | private List _sentMessages = new List(); 563 | 564 | public void Send(string message) 565 | { 566 | _sentMessages.Add(message); 567 | } 568 | 569 | public BusSpy ShouldSendNumberOfMessages(int number) 570 | { 571 | Assert.Equal(number, _sentMessages.Count); 572 | return this; 573 | } 574 | 575 | public BusSpy WithEmailChangedMessage(int userId, string newEmail) 576 | { 577 | string message = "Type: USER EMAIL CHANGED; " + 578 | $"Id: {userId}; " + 579 | $"NewEmail: {newEmail}"; 580 | Assert.Contains(_sentMessages, x => x == message); 581 | 582 | return this; 583 | } 584 | } 585 | } 586 | -------------------------------------------------------------------------------- /Book/Chapter10/V1/V1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.SqlClient; 4 | using System.Linq; 5 | using Dapper; 6 | using Moq; 7 | using Xunit; 8 | 9 | namespace Book.Chapter10.V1 10 | { 11 | public class User 12 | { 13 | public int UserId { get; set; } 14 | public string Email { get; private set; } 15 | public UserType Type { get; private set; } 16 | public bool IsEmailConfirmed { get; } 17 | public List DomainEvents { get; } 18 | 19 | public User(int userId, string email, UserType type, bool isEmailConfirmed) 20 | { 21 | UserId = userId; 22 | Email = email; 23 | Type = type; 24 | IsEmailConfirmed = isEmailConfirmed; 25 | DomainEvents = new List(); 26 | } 27 | 28 | public string CanChangeEmail() 29 | { 30 | if (IsEmailConfirmed) 31 | return "Can't change email after it's confirmed"; 32 | 33 | return null; 34 | } 35 | 36 | public void ChangeEmail(string newEmail, Company company) 37 | { 38 | Precondition.Requires(CanChangeEmail() == null); 39 | 40 | if (Email == newEmail) 41 | return; 42 | 43 | UserType newType = company.IsEmailCorporate(newEmail) 44 | ? UserType.Employee 45 | : UserType.Customer; 46 | 47 | if (Type != newType) 48 | { 49 | int delta = newType == UserType.Employee ? 1 : -1; 50 | company.ChangeNumberOfEmployees(delta); 51 | AddDomainEvent(new UserTypeChangedEvent(UserId, Type, newType)); 52 | } 53 | 54 | Email = newEmail; 55 | Type = newType; 56 | AddDomainEvent(new EmailChangedEvent(UserId, newEmail)); 57 | } 58 | 59 | private void AddDomainEvent(IDomainEvent domainEvent) 60 | { 61 | DomainEvents.Add(domainEvent); 62 | } 63 | } 64 | 65 | public class UserController 66 | { 67 | private readonly Database _database; 68 | private readonly EventDispatcher _eventDispatcher; 69 | 70 | public UserController( 71 | Database database, 72 | MessageBus messageBus, 73 | IDomainLogger domainLogger) 74 | { 75 | _database = database; 76 | _eventDispatcher = new EventDispatcher( 77 | messageBus, domainLogger); 78 | } 79 | 80 | public string ChangeEmail(int userId, string newEmail) 81 | { 82 | object[] userData = _database.GetUserById(userId); 83 | User user = UserFactory.Create(userData); 84 | 85 | string error = user.CanChangeEmail(); 86 | if (error != null) 87 | return error; 88 | 89 | object[] companyData = _database.GetCompany(); 90 | Company company = CompanyFactory.Create(companyData); 91 | 92 | user.ChangeEmail(newEmail, company); 93 | 94 | _database.SaveCompany(company); 95 | _database.SaveUser(user); 96 | _eventDispatcher.Dispatch(user.DomainEvents); 97 | 98 | return "OK"; 99 | } 100 | } 101 | 102 | public class EventDispatcher 103 | { 104 | private readonly MessageBus _messageBus; 105 | private readonly IDomainLogger _domainLogger; 106 | 107 | public EventDispatcher( 108 | MessageBus messageBus, 109 | IDomainLogger domainLogger) 110 | { 111 | _domainLogger = domainLogger; 112 | _messageBus = messageBus; 113 | } 114 | 115 | public void Dispatch(List events) 116 | { 117 | foreach (IDomainEvent ev in events) 118 | { 119 | Dispatch(ev); 120 | } 121 | } 122 | 123 | private void Dispatch(IDomainEvent ev) 124 | { 125 | switch (ev) 126 | { 127 | case EmailChangedEvent emailChangedEvent: 128 | _messageBus.SendEmailChangedMessage( 129 | emailChangedEvent.UserId, 130 | emailChangedEvent.NewEmail); 131 | break; 132 | 133 | case UserTypeChangedEvent userTypeChangedEvent: 134 | _domainLogger.UserTypeHasChanged( 135 | userTypeChangedEvent.UserId, 136 | userTypeChangedEvent.OldType, 137 | userTypeChangedEvent.NewType); 138 | break; 139 | } 140 | } 141 | } 142 | 143 | public class UserFactory 144 | { 145 | public static User Create(object[] data) 146 | { 147 | Precondition.Requires(data.Length >= 3); 148 | 149 | int id = (int)data[0]; 150 | string email = (string)data[1]; 151 | UserType type = (UserType)data[2]; 152 | bool isEmailConfirmed = (bool)data[3]; 153 | 154 | return new User(id, email, type, isEmailConfirmed); 155 | } 156 | } 157 | 158 | public class CompanyFactory 159 | { 160 | public static Company Create(object[] data) 161 | { 162 | Precondition.Requires(data.Length >= 2); 163 | 164 | string domainName = (string)data[0]; 165 | int numberOfEmployees = (int)data[1]; 166 | 167 | return new Company(domainName, numberOfEmployees); 168 | } 169 | } 170 | 171 | public interface IDomainLogger 172 | { 173 | void UserTypeHasChanged(int userId, UserType oldType, UserType newType); 174 | } 175 | 176 | public class DomainLogger : IDomainLogger 177 | { 178 | private readonly ILogger _logger; 179 | 180 | public DomainLogger(ILogger logger) 181 | { 182 | _logger = logger; 183 | } 184 | 185 | public void UserTypeHasChanged( 186 | int userId, UserType oldType, UserType newType) 187 | { 188 | _logger.Info( 189 | $"User {userId} changed type " + 190 | $"from {oldType} to {newType}"); 191 | } 192 | } 193 | 194 | public interface ILogger 195 | { 196 | void Info(string s); 197 | } 198 | 199 | public class UserTypeChangedEvent : IDomainEvent 200 | { 201 | public int UserId { get; } 202 | public UserType OldType { get; } 203 | public UserType NewType { get; } 204 | 205 | public UserTypeChangedEvent(int userId, UserType oldType, UserType newType) 206 | { 207 | UserId = userId; 208 | OldType = oldType; 209 | NewType = newType; 210 | } 211 | 212 | protected bool Equals(UserTypeChangedEvent other) 213 | { 214 | return UserId == other.UserId && string.Equals(OldType, other.OldType); 215 | } 216 | 217 | public override bool Equals(object obj) 218 | { 219 | if (ReferenceEquals(null, obj)) 220 | { 221 | return false; 222 | } 223 | 224 | if (ReferenceEquals(this, obj)) 225 | { 226 | return true; 227 | } 228 | 229 | if (obj.GetType() != this.GetType()) 230 | { 231 | return false; 232 | } 233 | 234 | return Equals((EmailChangedEvent)obj); 235 | } 236 | 237 | public override int GetHashCode() 238 | { 239 | unchecked 240 | { 241 | return (UserId * 397) ^ OldType.GetHashCode(); 242 | } 243 | } 244 | } 245 | 246 | public class EmailChangedEvent : IDomainEvent 247 | { 248 | public int UserId { get; } 249 | public string NewEmail { get; } 250 | 251 | public EmailChangedEvent(int userId, string newEmail) 252 | { 253 | UserId = userId; 254 | NewEmail = newEmail; 255 | } 256 | 257 | protected bool Equals(EmailChangedEvent other) 258 | { 259 | return UserId == other.UserId && string.Equals(NewEmail, other.NewEmail); 260 | } 261 | 262 | public override bool Equals(object obj) 263 | { 264 | if (ReferenceEquals(null, obj)) 265 | { 266 | return false; 267 | } 268 | 269 | if (ReferenceEquals(this, obj)) 270 | { 271 | return true; 272 | } 273 | 274 | if (obj.GetType() != this.GetType()) 275 | { 276 | return false; 277 | } 278 | 279 | return Equals((EmailChangedEvent)obj); 280 | } 281 | 282 | public override int GetHashCode() 283 | { 284 | unchecked 285 | { 286 | return (UserId * 397) ^ (NewEmail != null ? NewEmail.GetHashCode() : 0); 287 | } 288 | } 289 | } 290 | 291 | public interface IDomainEvent 292 | { 293 | } 294 | 295 | public class Company 296 | { 297 | public string DomainName { get; } 298 | public int NumberOfEmployees { get; private set; } 299 | 300 | public Company(string domainName, int numberOfEmployees) 301 | { 302 | DomainName = domainName; 303 | NumberOfEmployees = numberOfEmployees; 304 | } 305 | 306 | public void ChangeNumberOfEmployees(int delta) 307 | { 308 | Precondition.Requires(NumberOfEmployees + delta >= 0); 309 | 310 | NumberOfEmployees += delta; 311 | } 312 | 313 | public bool IsEmailCorporate(string email) 314 | { 315 | string emailDomain = email.Split('@')[1]; 316 | return emailDomain == DomainName; 317 | } 318 | } 319 | 320 | public enum UserType 321 | { 322 | Customer = 1, 323 | Employee = 2 324 | } 325 | 326 | public static class Precondition 327 | { 328 | public static void Requires(bool precondition, string message = null) 329 | { 330 | if (precondition == false) 331 | throw new Exception(message); 332 | } 333 | } 334 | 335 | public class UserControllerTests 336 | { 337 | private const string ConnectionString = @"Server=.\Sql;Database=IntegrationTests;Trusted_Connection=true;"; 338 | 339 | [Fact] 340 | public void Changing_email_from_corporate_to_non_corporate() 341 | { 342 | // Arrange 343 | var db = new Database(ConnectionString); 344 | User user = CreateUser( 345 | "user@mycorp.com", UserType.Employee, db); 346 | CreateCompany("mycorp.com", 1, db); 347 | 348 | var busSpy = new BusSpy(); 349 | var messageBus = new MessageBus(busSpy); 350 | var loggerMock = new Mock(); 351 | var sut = new UserController(db, messageBus, loggerMock.Object); 352 | 353 | // Act 354 | string result = sut.ChangeEmail(user.UserId, "new@gmail.com"); 355 | 356 | // Assert 357 | Assert.Equal("OK", result); 358 | 359 | object[] userData = db.GetUserById(user.UserId); 360 | User userFromDb = UserFactory.Create(userData); 361 | Assert.Equal("new@gmail.com", userFromDb.Email); 362 | Assert.Equal(UserType.Customer, userFromDb.Type); 363 | 364 | object[] companyData = db.GetCompany(); 365 | Company companyFromDb = CompanyFactory.Create(companyData); 366 | Assert.Equal(0, companyFromDb.NumberOfEmployees); 367 | 368 | busSpy.ShouldSendNumberOfMessages(1) 369 | .WithEmailChangedMessage(user.UserId, "new@gmail.com"); 370 | loggerMock.Verify( 371 | x => x.UserTypeHasChanged( 372 | user.UserId, UserType.Employee, UserType.Customer), 373 | Times.Once); 374 | } 375 | 376 | private Company CreateCompany(string domainName, int numberOfEmployees, Database database) 377 | { 378 | var company = new Company(domainName, numberOfEmployees); 379 | database.SaveCompany(company); 380 | return company; 381 | } 382 | 383 | private User CreateUser(string email, UserType type, Database database) 384 | { 385 | var user = new User(0, email, type, false); 386 | database.SaveUser(user); 387 | return user; 388 | } 389 | } 390 | 391 | public class Database 392 | { 393 | private readonly string _connectionString; 394 | 395 | public Database(string connectionString) 396 | { 397 | _connectionString = connectionString; 398 | } 399 | 400 | public object[] GetUserById(int userId) 401 | { 402 | using (SqlConnection connection = new SqlConnection(_connectionString)) 403 | { 404 | string query = "SELECT * FROM [dbo].[User] WHERE UserID = @UserID"; 405 | dynamic data = connection.QuerySingle(query, new { UserID = userId }); 406 | 407 | return new object[] 408 | { 409 | data.UserID, 410 | data.Email, 411 | data.Type, 412 | data.IsEmailConfirmed 413 | }; 414 | } 415 | } 416 | 417 | public void SaveUser(User user) 418 | { 419 | using (var connection = new SqlConnection(_connectionString)) 420 | { 421 | string updateQuery = @" 422 | UPDATE [dbo].[User] 423 | SET Email = @Email, Type = @Type, 424 | IsEmailConfirmed = @IsEmailConfirmed 425 | WHERE UserID = @UserID 426 | SELECT @UserID"; 427 | 428 | string insertQuery = @" 429 | INSERT [dbo].[User] (Email, Type, IsEmailConfirmed) 430 | VALUES (@Email, @Type, @IsEmailConfirmed) 431 | SELECT CAST(SCOPE_IDENTITY() as int)"; 432 | 433 | string query = user.UserId == 0 434 | ? insertQuery 435 | : updateQuery; 436 | int userId = connection.Query(query, new 437 | { 438 | user.Email, 439 | user.UserId, 440 | user.IsEmailConfirmed, 441 | Type = (int)user.Type 442 | }) 443 | .Single(); 444 | 445 | user.UserId = userId; 446 | } 447 | } 448 | 449 | public object[] GetCompany() 450 | { 451 | using (SqlConnection connection = new SqlConnection(_connectionString)) 452 | { 453 | string query = "SELECT * FROM dbo.Company"; 454 | dynamic data = connection.QuerySingle(query); 455 | 456 | return new object[] 457 | { 458 | data.DomainName, 459 | data.NumberOfEmployees 460 | }; 461 | } 462 | } 463 | 464 | public void SaveCompany(Company company) 465 | { 466 | using (var connection = new SqlConnection(_connectionString)) 467 | { 468 | string query = @" 469 | UPDATE dbo.Company 470 | SET DomainName = @DomainName, 471 | NumberOfEmployees = @NumberOfEmployees"; 472 | 473 | connection.Execute(query, new 474 | { 475 | company.DomainName, 476 | company.NumberOfEmployees 477 | }); 478 | } 479 | } 480 | } 481 | 482 | public class MessageBus 483 | { 484 | private readonly IBus _bus; 485 | 486 | public MessageBus(IBus bus) 487 | { 488 | _bus = bus; 489 | } 490 | 491 | public void SendEmailChangedMessage(int userId, string newEmail) 492 | { 493 | _bus.Send("Type: USER EMAIL CHANGED; " + 494 | $"Id: {userId}; " + 495 | $"NewEmail: {newEmail}"); 496 | } 497 | } 498 | 499 | public interface IBus 500 | { 501 | void Send(string message); 502 | } 503 | 504 | public class BusSpy : IBus 505 | { 506 | private List _sentMessages = new List(); 507 | 508 | public void Send(string message) 509 | { 510 | _sentMessages.Add(message); 511 | } 512 | 513 | public BusSpy ShouldSendNumberOfMessages(int number) 514 | { 515 | Assert.Equal(number, _sentMessages.Count); 516 | return this; 517 | } 518 | 519 | public BusSpy WithEmailChangedMessage(int userId, string newEmail) 520 | { 521 | string message = "Type: USER EMAIL CHANGED; " + 522 | $"Id: {userId}; " + 523 | $"NewEmail: {newEmail}"; 524 | Assert.Contains(_sentMessages, x => x == message); 525 | 526 | return this; 527 | } 528 | } 529 | } 530 | -------------------------------------------------------------------------------- /Book/Chapter11/CodePollution/CodePollution.cs: -------------------------------------------------------------------------------- 1 | using Book.Chapter11.LeakingKnowledge; 2 | using Xunit; 3 | 4 | namespace Book.Chapter11.CodePollution 5 | { 6 | public class Logger 7 | { 8 | private readonly bool _isTestEnvironment; 9 | 10 | public Logger(bool isTestEnvironment) 11 | { 12 | _isTestEnvironment = isTestEnvironment; 13 | } 14 | 15 | public void Log(string text) 16 | { 17 | if (_isTestEnvironment) 18 | return; 19 | 20 | /* Log the text */ 21 | } 22 | } 23 | 24 | public class Controller 25 | { 26 | public void SomeMethod(Logger logger) 27 | { 28 | logger.Log("SomeMethod is called"); 29 | } 30 | } 31 | 32 | public class Tests 33 | { 34 | [Fact] 35 | public void Some_test() 36 | { 37 | var logger = new Logger(true); 38 | var sut = new Controller(); 39 | 40 | sut.SomeMethod(logger); 41 | 42 | /* assert */ 43 | } 44 | } 45 | 46 | public interface ILogger 47 | { 48 | void Log(string text); 49 | } 50 | 51 | public class Logger2 : ILogger 52 | { 53 | public void Log(string text) 54 | { 55 | /* Log the text */ 56 | } 57 | } 58 | 59 | public class FakeLogger : ILogger 60 | { 61 | public void Log(string text) 62 | { 63 | /* Do nothing */ 64 | } 65 | } 66 | 67 | public class Controller2 68 | { 69 | public void SomeMethod(ILogger logger) 70 | { 71 | logger.Log("SomeMethod is called"); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Book/Chapter11/LeakingKnowledge/LeakingKnowledge.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Book.Chapter11.LeakingKnowledge 4 | { 5 | public static class Calculator 6 | { 7 | public static int Add(int value1, int value2) 8 | { 9 | return value1 + value2; 10 | } 11 | } 12 | 13 | public class CalculatorTests 14 | { 15 | [Fact] 16 | public void Adding_two_numbers() 17 | { 18 | int value1 = 1; 19 | int value2 = 3; 20 | int expected = value1 + value2; 21 | 22 | int actual = Calculator.Add(value1, value2); 23 | 24 | Assert.Equal(expected, actual); 25 | } 26 | } 27 | 28 | public class CalculatorTests2 29 | { 30 | [Theory] 31 | [InlineData(1, 3)] 32 | [InlineData(11, 33)] 33 | [InlineData(100, 500)] 34 | public void Adding_two_numbers(int value1, int value2) 35 | { 36 | int expected = value1 + value2; 37 | 38 | int actual = Calculator.Add(value1, value2); 39 | 40 | Assert.Equal(expected, actual); 41 | } 42 | } 43 | 44 | public class CalculatorTests4 45 | { 46 | [Theory] 47 | [InlineData(1, 3, 4)] 48 | [InlineData(11, 33, 44)] 49 | [InlineData(100, 500, 600)] 50 | public void Adding_two_numbers(int value1, int value2, int expected) 51 | { 52 | int actual = Calculator.Add(value1, value2); 53 | Assert.Equal(expected, actual); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Book/Chapter11/MockingClasses/MockingClasses.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Moq; 5 | using Xunit; 6 | 7 | namespace Book.Chapter11.MockingClasses 8 | { 9 | public class StatisticsCalculator 10 | { 11 | public (double totalWeight, double totalCost) Calculate( 12 | int customerId) 13 | { 14 | List records = GetDeliveries(customerId); 15 | 16 | double totalWeight = records.Sum(x => x.Weight); 17 | double totalCost = records.Sum(x => x.Cost); 18 | 19 | return (totalWeight, totalCost); 20 | } 21 | 22 | public virtual List GetDeliveries(int customerId) 23 | { 24 | /* Call an out-of-process dependency 25 | to get the list of deliveries */ 26 | return new List(); 27 | } 28 | } 29 | 30 | public class DeliveryRecord 31 | { 32 | public double Weight { get; set; } 33 | public double Cost { get; set; } 34 | } 35 | 36 | public class CustomerController 37 | { 38 | private readonly StatisticsCalculator _calculator; 39 | 40 | public CustomerController(StatisticsCalculator calculator) 41 | { 42 | _calculator = calculator; 43 | } 44 | 45 | public string GetStatistics(int customerId) 46 | { 47 | (double totalWeight, double totalCost) = _calculator 48 | .Calculate(customerId); 49 | 50 | return 51 | $"Total weight delivered: {totalWeight}. " + 52 | $"Total cost: {totalCost}"; 53 | } 54 | } 55 | 56 | public class Tests 57 | { 58 | [Fact] 59 | public void Customer_with_no_deliveries() 60 | { 61 | // Arrange 62 | var stub = new Mock { CallBase = true }; 63 | stub.Setup(x => x.GetDeliveries(1)) 64 | .Returns(new List()); 65 | var sut = new CustomerController(stub.Object); 66 | 67 | // Act 68 | string result = sut.GetStatistics(1); 69 | 70 | // Assert 71 | Assert.Equal("Total weight delivered: 0. Total cost: 0", result); 72 | } 73 | } 74 | 75 | public class DeliveryGateway : IDeliveryGateway 76 | { 77 | public List GetDeliveries(int customerId) 78 | { 79 | /* Call an out-of-process dependency 80 | to get the list of deliveries */ 81 | return new List(); 82 | } 83 | } 84 | 85 | public interface IDeliveryGateway 86 | { 87 | List GetDeliveries(int customerId); 88 | } 89 | 90 | public class StatisticsCalculator2 91 | { 92 | public (double totalWeight, double totalCost) Calculate( 93 | List records) 94 | { 95 | double totalWeight = records.Sum(x => x.Weight); 96 | double totalCost = records.Sum(x => x.Cost); 97 | 98 | return (totalWeight, totalCost); 99 | } 100 | } 101 | 102 | public class CustomerController2 103 | { 104 | private readonly StatisticsCalculator2 _calculator; 105 | private readonly IDeliveryGateway _gateway; 106 | 107 | public CustomerController2(StatisticsCalculator2 calculator, IDeliveryGateway gateway) 108 | { 109 | _calculator = calculator; 110 | _gateway = gateway; 111 | } 112 | 113 | public string GetStatistics(int customerId) 114 | { 115 | List records = _gateway.GetDeliveries(customerId); 116 | (double totalWeight, double totalCost) = _calculator.Calculate(records); 117 | 118 | return $"Total weight delivered: {totalWeight}. Total cost: {totalCost}"; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Book/Chapter11/PrivateMethods/PrivateMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Book.Chapter11.PrivateMethods 5 | { 6 | public class Order 7 | { 8 | private Customer _customer; 9 | private List _products; 10 | 11 | public string GenerateDescription() 12 | { 13 | return $"Customer name: {_customer.Name}, " + 14 | $"total number of products: {_products.Count}, " + 15 | $"total price: {GetPrice()}"; 16 | } 17 | 18 | private decimal GetPrice() 19 | { 20 | decimal basePrice = /* Calculate based on _products */ 0; 21 | decimal discounts = /* Calculate based on _customer */ 0; 22 | decimal taxes = /* Calculate based on _products */ 0; 23 | return basePrice - discounts + taxes; 24 | } 25 | } 26 | 27 | public class Product 28 | { 29 | } 30 | 31 | public class Customer 32 | { 33 | public object Name { get; set; } 34 | } 35 | 36 | public class OrderV2 37 | { 38 | private Customer _customer; 39 | private List _products; 40 | 41 | public string GenerateDescription() 42 | { 43 | var calculator = new PriceCalculator(); 44 | 45 | return $"Customer name: {_customer.Name}, " + 46 | $"total number of products: {_products.Count}, " + 47 | $"total price: {calculator.Calculate(_customer, _products)}"; 48 | } 49 | } 50 | 51 | public class PriceCalculator 52 | { 53 | public decimal Calculate(Customer customer, List products) 54 | { 55 | decimal basePrice = /* Calculate based on products */ 0; 56 | decimal discounts = /* Calculate based on customer */ 0; 57 | decimal taxes = /* Calculate based on products */ 0; 58 | return basePrice - discounts + taxes; 59 | } 60 | } 61 | 62 | public class Inquiry 63 | { 64 | public bool IsApproved { get; private set; } 65 | public DateTime? TimeApproved { get; private set; } 66 | 67 | private Inquiry(bool isApproved, DateTime? timeApproved) 68 | { 69 | if (isApproved && !timeApproved.HasValue) 70 | throw new Exception(); 71 | 72 | IsApproved = isApproved; 73 | TimeApproved = timeApproved; 74 | } 75 | 76 | public void Approve(DateTime now) 77 | { 78 | if (IsApproved) 79 | return; 80 | 81 | IsApproved = true; 82 | TimeApproved = now; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Book/Chapter11/PrivateState/PrivateState.cs: -------------------------------------------------------------------------------- 1 | namespace Book.Chapter11.PrivateState 2 | { 3 | public class Customer 4 | { 5 | private CustomerStatus _status = CustomerStatus.Regular; 6 | 7 | public void Promote() 8 | { 9 | _status = CustomerStatus.Preferred; 10 | } 11 | 12 | public decimal GetDiscount() 13 | { 14 | return _status == CustomerStatus.Preferred ? 0.05m : 0m; 15 | } 16 | } 17 | 18 | public enum CustomerStatus 19 | { 20 | Regular, 21 | Preferred 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Book/Chapter11/Time/Ambient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Book.Chapter11.Time 4 | { 5 | public static class DateTimeServer 6 | { 7 | private static Func _func; 8 | public static DateTime Now => _func(); 9 | 10 | public static void Init(Func func) 11 | { 12 | _func = func; 13 | } 14 | } 15 | 16 | /* 17 | // Initialization code for production 18 | DateTimeServer.Init(() => DateTime.UtcNow); 19 | 20 | // Initialization code for unit tests 21 | DateTimeServer.Init(() => new DateTime(2016, 5, 3)); 22 | */ 23 | } 24 | -------------------------------------------------------------------------------- /Book/Chapter11/Time/DI.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Book.Chapter11.Time 4 | { 5 | public interface IDateTimeServer 6 | { 7 | DateTime Now { get; } 8 | } 9 | 10 | public class DateTimeServer2 : IDateTimeServer 11 | { 12 | public DateTime Now => DateTime.Now; 13 | } 14 | 15 | public class InquiryController 16 | { 17 | private readonly DateTimeServer2 _dateTimeServer; 18 | 19 | public InquiryController(DateTimeServer2 dateTimeServer) 20 | { 21 | _dateTimeServer = dateTimeServer; 22 | } 23 | 24 | public void ApproveInquiry(int id) 25 | { 26 | Inquiry inquiry = GetById(id); 27 | inquiry.Approve(_dateTimeServer.Now); 28 | SaveInquiry(inquiry); 29 | } 30 | 31 | private void SaveInquiry(Inquiry inquiry) 32 | { 33 | } 34 | 35 | private Inquiry GetById(int id) 36 | { 37 | return null; 38 | } 39 | } 40 | 41 | public class Inquiry 42 | { 43 | public bool IsApproved { get; private set; } 44 | public DateTime? TimeApproved { get; private set; } 45 | 46 | private Inquiry(bool isApproved, DateTime? timeApproved) 47 | { 48 | if (isApproved && !timeApproved.HasValue) 49 | throw new Exception(); 50 | 51 | IsApproved = isApproved; 52 | TimeApproved = timeApproved; 53 | } 54 | 55 | public void Approve(DateTime now) 56 | { 57 | if (IsApproved) 58 | return; 59 | 60 | IsApproved = true; 61 | TimeApproved = now; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Book/Chapter2/Listing1/CustomerTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Book.Chapter2.Listing1 4 | { 5 | public class CustomerTests 6 | { 7 | [Fact] 8 | public void Purchase_succeeds_when_enough_inventory() 9 | { 10 | // Arrange 11 | var store = new Store(); 12 | store.AddInventory(Product.Shampoo, 10); 13 | var customer = new Customer(); 14 | 15 | // Act 16 | bool success = customer.Purchase(store, Product.Shampoo, 5); 17 | 18 | // Assert 19 | Assert.True(success); 20 | Assert.Equal(5, store.GetInventory(Product.Shampoo)); 21 | } 22 | 23 | [Fact] 24 | public void Purchase_fails_when_not_enough_inventory() 25 | { 26 | // Arrange 27 | var store = new Store(); 28 | store.AddInventory(Product.Shampoo, 10); 29 | var customer = new Customer(); 30 | 31 | // Act 32 | bool success = customer.Purchase(store, Product.Shampoo, 15); 33 | 34 | // Assert 35 | Assert.False(success); 36 | Assert.Equal(10, store.GetInventory(Product.Shampoo)); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Book/Chapter2/Listing1/Other.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Book.Chapter2.Listing1 5 | { 6 | public class Store 7 | { 8 | private readonly Dictionary _inventory = new Dictionary(); 9 | 10 | public bool HasEnoughInventory(Product product, int quantity) 11 | { 12 | return GetInventory(product) >= quantity; 13 | } 14 | 15 | public void RemoveInventory(Product product, int quantity) 16 | { 17 | if (!HasEnoughInventory(product, quantity)) 18 | { 19 | throw new Exception("Not enough inventory"); 20 | } 21 | 22 | _inventory[product] -= quantity; 23 | } 24 | 25 | public void AddInventory(Product product, int quantity) 26 | { 27 | if (_inventory.ContainsKey(product)) 28 | { 29 | _inventory[product] += quantity; 30 | } 31 | else 32 | { 33 | _inventory.Add(product, quantity); 34 | } 35 | } 36 | 37 | public int GetInventory(Product product) 38 | { 39 | bool productExists = _inventory.TryGetValue(product, out int remaining); 40 | return productExists ? remaining : 0; 41 | } 42 | } 43 | 44 | public enum Product 45 | { 46 | Shampoo, 47 | Book 48 | } 49 | 50 | public class Customer 51 | { 52 | public bool Purchase(Store store, Product product, int quantity) 53 | { 54 | if (!store.HasEnoughInventory(product, quantity)) 55 | { 56 | return false; 57 | } 58 | 59 | store.RemoveInventory(product, quantity); 60 | 61 | return true; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Book/Chapter2/Listing2/CustomerTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using Xunit; 3 | 4 | namespace Book.Chapter2.Listing2 5 | { 6 | public class CustomerTests 7 | { 8 | [Fact] 9 | public void Purchase_succeeds_when_enough_inventory() 10 | { 11 | // Arrange 12 | var storeMock = new Mock(); 13 | storeMock 14 | .Setup(x => x.HasEnoughInventory(Product.Shampoo, 5)) 15 | .Returns(true); 16 | var customer = new Customer(); 17 | 18 | // Act 19 | bool success = customer.Purchase(storeMock.Object, Product.Shampoo, 5); 20 | 21 | // Assert 22 | Assert.True(success); 23 | storeMock.Verify(x => x.RemoveInventory(Product.Shampoo, 5), Times.Once); 24 | } 25 | 26 | [Fact] 27 | public void Purchase_fails_when_not_enough_inventory() 28 | { 29 | // Arrange 30 | var storeMock = new Mock(); 31 | storeMock 32 | .Setup(x => x.HasEnoughInventory(Product.Shampoo, 5)) 33 | .Returns(false); 34 | var customer = new Customer(); 35 | 36 | // Act 37 | bool success = customer.Purchase(storeMock.Object, Product.Shampoo, 5); 38 | 39 | // Assert 40 | Assert.False(success); 41 | storeMock.Verify(x => x.RemoveInventory(Product.Shampoo, 5), Times.Never); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Book/Chapter2/Listing2/Other.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Book.Chapter2.Listing2 5 | { 6 | public class Store : IStore 7 | { 8 | private readonly Dictionary _inventory = new Dictionary(); 9 | 10 | public bool HasEnoughInventory(Product product, int quantity) 11 | { 12 | return GetInventory(product) >= quantity; 13 | } 14 | 15 | public void RemoveInventory(Product product, int quantity) 16 | { 17 | if (!HasEnoughInventory(product, quantity)) 18 | { 19 | throw new Exception("Not enough inventory"); 20 | } 21 | 22 | _inventory[product] -= quantity; 23 | } 24 | 25 | public void AddInventory(Product product, int quantity) 26 | { 27 | if (_inventory.ContainsKey(product)) 28 | { 29 | _inventory[product] += quantity; 30 | } 31 | else 32 | { 33 | _inventory.Add(product, quantity); 34 | } 35 | } 36 | 37 | public int GetInventory(Product product) 38 | { 39 | bool productExists = _inventory.TryGetValue(product, out int remaining); 40 | return productExists ? remaining : 0; 41 | } 42 | } 43 | 44 | public interface IStore 45 | { 46 | bool HasEnoughInventory(Product product, int quantity); 47 | void RemoveInventory(Product product, int quantity); 48 | void AddInventory(Product product, int quantity); 49 | int GetInventory(Product product); 50 | } 51 | 52 | public enum Product 53 | { 54 | Shampoo, 55 | Book 56 | } 57 | 58 | public class Customer 59 | { 60 | public bool Purchase(IStore store, Product product, int quantity) 61 | { 62 | if (!store.HasEnoughInventory(product, quantity)) 63 | { 64 | return false; 65 | } 66 | 67 | store.RemoveInventory(product, quantity); 68 | 69 | return true; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Book/Chapter3/CustomerTests_3/CustomerTests.cs: -------------------------------------------------------------------------------- 1 | using Book.Chapter2.Listing1; 2 | using Xunit; 3 | 4 | namespace Book.Chapter3.CustomerTests_3 5 | { 6 | public class CustomerTests 7 | { 8 | private readonly Store _store; 9 | private readonly Customer _sut; 10 | 11 | public CustomerTests() 12 | { 13 | _store = new Store(); 14 | _store.AddInventory(Product.Shampoo, 10); 15 | _sut = new Customer(); 16 | } 17 | 18 | [Fact] 19 | public void Purchase_succeeds_when_enough_inventory() 20 | { 21 | bool success = _sut.Purchase(_store, Product.Shampoo, 5); 22 | 23 | Assert.True(success); 24 | Assert.Equal(5, _store.GetInventory(Product.Shampoo)); 25 | } 26 | 27 | [Fact] 28 | public void Purchase_fails_when_not_enough_inventory() 29 | { 30 | bool success = _sut.Purchase(_store, Product.Shampoo, 15); 31 | 32 | Assert.False(success); 33 | Assert.Equal(10, _store.GetInventory(Product.Shampoo)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Book/Chapter3/CustomerTests_4/CustomerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Book.Chapter2.Listing1; 3 | using Xunit; 4 | 5 | namespace Book.Chapter3.CustomerTests_4 6 | { 7 | public class CustomerTests 8 | { 9 | [Fact] 10 | public void Purchase_succeeds_when_enough_inventory() 11 | { 12 | Store store = CreateStoreWithInventory(Product.Shampoo, 10); 13 | Customer sut = CreateCustomer(); 14 | 15 | bool success = sut.Purchase(store, Product.Shampoo, 5); 16 | 17 | Assert.True(success); 18 | Assert.Equal(5, store.GetInventory(Product.Shampoo)); 19 | } 20 | 21 | [Fact] 22 | public void Purchase_fails_when_not_enough_inventory() 23 | { 24 | Store store = CreateStoreWithInventory(Product.Shampoo, 10); 25 | Customer sut = CreateCustomer(); 26 | 27 | bool success = sut.Purchase(store, Product.Shampoo, 15); 28 | 29 | Assert.False(success); 30 | Assert.Equal(10, store.GetInventory(Product.Shampoo)); 31 | } 32 | 33 | private Store CreateStoreWithInventory(Product product, int quantity) 34 | { 35 | Store store = new Store(); 36 | store.AddInventory(product, quantity); 37 | return store; 38 | } 39 | 40 | private static Customer CreateCustomer() 41 | { 42 | return new Customer(); 43 | } 44 | } 45 | 46 | public class CustomerTests2 : IntegrationTests 47 | { 48 | [Fact] 49 | public void Purchase_succeeds_when_enough_inventory() 50 | { 51 | /* ... */ 52 | } 53 | } 54 | 55 | public abstract class IntegrationTests : IDisposable 56 | { 57 | protected readonly Database _database; 58 | 59 | protected IntegrationTests() 60 | { 61 | _database = new Database(); 62 | } 63 | 64 | public void Dispose() 65 | { 66 | _database.Dispose(); 67 | } 68 | } 69 | 70 | public class Database 71 | { 72 | public void Dispose() 73 | { 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Book/Chapter3/FluentAssertions_1/CalculatorTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Xunit; 3 | 4 | namespace Book.Chapter3.FluentAssertions_1 5 | { 6 | public class CalculatorTests 7 | { 8 | [Fact] 9 | public void Sum_of_two_numbers() 10 | { 11 | double first = 10; 12 | double second = 20; 13 | var sut = new Calculator(); 14 | 15 | double result = sut.Sum(first, second); 16 | 17 | result.Should().Be(30); 18 | } 19 | } 20 | 21 | public class Calculator 22 | { 23 | public double Sum(double first, double second) 24 | { 25 | return first + second; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Book/Chapter3/Listing1/CalculatorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace Book.Chapter3.Listing1 5 | { 6 | public class CalculatorTests 7 | { 8 | [Fact] 9 | public void Sum_of_two_numbers() 10 | { 11 | // Arrange 12 | double first = 10; 13 | double second = 20; 14 | var calculator = new Calculator(); 15 | 16 | // Act 17 | double result = calculator.Sum(first, second); 18 | 19 | // Assert 20 | Assert.Equal(30, result); 21 | } 22 | } 23 | 24 | public class Calculator 25 | { 26 | public double Sum(double first, double second) 27 | { 28 | return first + second; 29 | } 30 | 31 | public void CleanUp() 32 | { 33 | } 34 | } 35 | 36 | public class CalculatorTests2 : IDisposable 37 | { 38 | private readonly Calculator _calculator; 39 | 40 | public CalculatorTests2() 41 | { 42 | _calculator = new Calculator(); 43 | } 44 | 45 | [Fact] 46 | public void Sum_of_two_numbers() 47 | { 48 | // Arrange 49 | double first = 10; 50 | double second = 20; 51 | 52 | // Act 53 | double result = _calculator.Sum(first, second); 54 | 55 | // Assert 56 | Assert.Equal(30, result); 57 | } 58 | 59 | public void Dispose() 60 | { 61 | _calculator.CleanUp(); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Book/Chapter3/Listing6/DeliveryServiceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Xunit; 4 | 5 | namespace Book.Chapter3.Listing6 6 | { 7 | public class DeliveryServiceTests 8 | { 9 | [InlineData(-1, false)] 10 | [InlineData(0, false)] 11 | [InlineData(1, false)] 12 | [InlineData(2, true)] 13 | [Theory] 14 | public void Detects_an_invalid_delivery_date(int daysFromNow, bool expected) 15 | { 16 | DeliveryService sut = new DeliveryService(); 17 | DateTime deliveryDate = DateTime.Now.AddDays(daysFromNow); 18 | Delivery delivery = new Delivery 19 | { 20 | Date = deliveryDate 21 | }; 22 | 23 | bool isValid = sut.IsDeliveryValid(delivery); 24 | 25 | Assert.Equal(expected, isValid); 26 | } 27 | 28 | [InlineData(-1)] 29 | [InlineData(0)] 30 | [InlineData(1)] 31 | [Theory] 32 | public void Detects_an_invalid_delivery_date2(int daysFromNow) 33 | { 34 | DeliveryService sut = new DeliveryService(); 35 | DateTime deliveryDate = DateTime.Now.AddDays(daysFromNow); 36 | Delivery delivery = new Delivery 37 | { 38 | Date = deliveryDate 39 | }; 40 | 41 | bool isValid = sut.IsDeliveryValid(delivery); 42 | 43 | Assert.False(isValid); 44 | } 45 | 46 | [Fact] 47 | public void The_soonest_delivery_date_is_two_days_from_now() 48 | { 49 | DeliveryService sut = new DeliveryService(); 50 | DateTime deliveryDate = DateTime.Now.AddDays(2); 51 | Delivery delivery = new Delivery 52 | { 53 | Date = deliveryDate 54 | }; 55 | 56 | bool isValid = sut.IsDeliveryValid(delivery); 57 | 58 | Assert.True(isValid); 59 | } 60 | 61 | [Theory] 62 | [MemberData(nameof(Data))] 63 | public void Detects_an_invalid_delivery_date3( 64 | DateTime deliveryDate, 65 | bool expected) 66 | { 67 | DeliveryService sut = new DeliveryService(); 68 | Delivery delivery = new Delivery 69 | { 70 | Date = deliveryDate 71 | }; 72 | 73 | bool isValid = sut.IsDeliveryValid(delivery); 74 | 75 | Assert.Equal(expected, isValid); 76 | } 77 | 78 | public static List Data() 79 | { 80 | return new List 81 | { 82 | new object[] { DateTime.Now.AddDays(-1), false }, 83 | new object[] { DateTime.Now, false }, 84 | new object[] { DateTime.Now.AddDays(1), false }, 85 | new object[] { DateTime.Now.AddDays(2), true } 86 | }; 87 | } 88 | } 89 | 90 | public class Delivery 91 | { 92 | public DateTime Date { get; set; } 93 | } 94 | 95 | public class DeliveryService 96 | { 97 | public bool IsDeliveryValid(Delivery delivery) 98 | { 99 | return delivery.Date >= DateTime.Now.AddDays(1.999); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Book/Chapter4/Listing1/MessageRendererTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using Xunit; 5 | 6 | namespace Book.Chapter4.Listing1 7 | { 8 | public class MessageRendererTests 9 | { 10 | [Fact] 11 | public void Rendering_a_message() 12 | { 13 | var sut = new MessageRenderer(); 14 | var message = new Message 15 | { 16 | Header = "h", 17 | Body = "b", 18 | Footer = "f" 19 | }; 20 | 21 | string html = sut.Render(message); 22 | 23 | Assert.Equal("

h

bf", html); 24 | } 25 | 26 | [Fact] 27 | public void MessageRenderer_uses_correct_sub_renderers() 28 | { 29 | var sut = new MessageRenderer(); 30 | 31 | IReadOnlyList renderers = sut.SubRenderers; 32 | 33 | Assert.Equal(3, renderers.Count); 34 | Assert.IsAssignableFrom(renderers[0]); 35 | Assert.IsAssignableFrom(renderers[1]); 36 | Assert.IsAssignableFrom(renderers[2]); 37 | } 38 | 39 | [Fact(Skip = "Example of how not to write tests")] 40 | public void MessageRenderer_is_implemented_correctly() 41 | { 42 | string sourceCode = File.ReadAllText(@"\MessageRenderer.cs"); 43 | 44 | Assert.Equal( 45 | @" 46 | public class MessageRenderer : IRenderer 47 | { 48 | public IReadOnlyList SubRenderers { get; } 49 | 50 | public MessageRenderer() 51 | { 52 | SubRenderers = new List 53 | { 54 | new HeaderRenderer(), 55 | new BodyRenderer(), 56 | new FooterRenderer() 57 | }; 58 | } 59 | 60 | public string Render(Message message) 61 | { 62 | return SubRenderers 63 | .Select(x => x.Render(message)) 64 | .Aggregate("", (str1, str2) => str1 + str2); 65 | } 66 | }", sourceCode); 67 | } 68 | } 69 | 70 | public class Message 71 | { 72 | public string Header { get; set; } 73 | public string Body { get; set; } 74 | public string Footer { get; set; } 75 | } 76 | 77 | public interface IRenderer 78 | { 79 | string Render(Message message); 80 | } 81 | 82 | public class MessageRenderer : IRenderer 83 | { 84 | public IReadOnlyList SubRenderers { get; } 85 | 86 | public MessageRenderer() 87 | { 88 | SubRenderers = new List 89 | { 90 | new HeaderRenderer(), 91 | new BodyRenderer(), 92 | new FooterRenderer() 93 | }; 94 | } 95 | 96 | public string Render(Message message) 97 | { 98 | return SubRenderers 99 | .Select(x => x.Render(message)) 100 | .Aggregate("", (str1, str2) => str1 + str2); 101 | } 102 | } 103 | 104 | public class FooterRenderer : IRenderer 105 | { 106 | public string Render(Message message) 107 | { 108 | return $"{message.Footer}"; 109 | } 110 | } 111 | 112 | public class BodyRenderer : IRenderer 113 | { 114 | public string Render(Message message) 115 | { 116 | return $"{message.Body}"; 117 | } 118 | } 119 | 120 | public class HeaderRenderer : IRenderer 121 | { 122 | public string Render(Message message) 123 | { 124 | return $"

{message.Header}

"; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Book/Chapter5/Listing1/Listing1.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using Xunit; 3 | 4 | namespace Book.Chapter5.Listing1 5 | { 6 | public class ControllerTests 7 | { 8 | [Fact] 9 | public void Sending_a_greetings_email() 10 | { 11 | var emailGatewayMock = new Mock(); 12 | var sut = new Controller(emailGatewayMock.Object); 13 | 14 | sut.GreetUser("user@email.com"); 15 | 16 | emailGatewayMock.Verify( 17 | x => x.SendGreetingsEmail("user@email.com"), 18 | Times.Once); 19 | } 20 | 21 | [Fact] 22 | public void Creating_a_report() 23 | { 24 | var stub = new Mock(); 25 | stub.Setup(x => x.GetNumberOfUsers()).Returns(10); 26 | var sut = new Controller(stub.Object); 27 | 28 | Report report = sut.CreateReport(); 29 | 30 | Assert.Equal(10, report.NumberOfUsers); 31 | } 32 | } 33 | 34 | public class Controller 35 | { 36 | private readonly IEmailGateway _emailGateway; 37 | private readonly IDatabase _database; 38 | 39 | public Controller(IEmailGateway emailGateway) 40 | { 41 | _emailGateway = emailGateway; 42 | } 43 | 44 | public Controller(IDatabase database) 45 | { 46 | _database = database; 47 | } 48 | 49 | public void GreetUser(string userEmail) 50 | { 51 | _emailGateway.SendGreetingsEmail(userEmail); 52 | } 53 | 54 | public Report CreateReport() 55 | { 56 | int numberOfUsers = _database.GetNumberOfUsers(); 57 | return new Report(numberOfUsers); 58 | } 59 | } 60 | 61 | public class Report 62 | { 63 | public int NumberOfUsers { get; } 64 | 65 | public Report(int numberOfUsers) 66 | { 67 | NumberOfUsers = numberOfUsers; 68 | } 69 | } 70 | 71 | public interface IDatabase 72 | { 73 | int GetNumberOfUsers(); 74 | } 75 | 76 | public interface IEmailGateway 77 | { 78 | void SendGreetingsEmail(string userEmail); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Book/Chapter5/Listing5/Listing5.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Book.Chapter5.Listing5 4 | { 5 | public class User 6 | { 7 | public string Name { get; set; } 8 | 9 | public string NormalizeName(string name) 10 | { 11 | string result = (name ?? "").Trim(); 12 | 13 | if (result.Length > 50) 14 | return result.Substring(0, 50); 15 | 16 | return result; 17 | } 18 | } 19 | 20 | public class UserController 21 | { 22 | public void RenameUser(int userId, string newName) 23 | { 24 | User user = GetUserFromDatabase(userId); 25 | 26 | string normalizedName = user.NormalizeName(newName); 27 | user.Name = normalizedName; 28 | 29 | SaveUserToDatabase(user); 30 | } 31 | 32 | private void SaveUserToDatabase(User user) 33 | { 34 | } 35 | 36 | private User GetUserFromDatabase(int userId) 37 | { 38 | return new User(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Book/Chapter5/Listing6/Listing6.cs: -------------------------------------------------------------------------------- 1 | namespace Book.Chapter5.Listing6 2 | { 3 | public class User 4 | { 5 | private string _name; 6 | public string Name 7 | { 8 | get => _name; 9 | set => _name = NormalizeName(value); 10 | } 11 | 12 | private string NormalizeName(string name) 13 | { 14 | string result = (name ?? "").Trim(); 15 | 16 | if (result.Length > 50) 17 | return result.Substring(0, 50); 18 | 19 | return result; 20 | } 21 | } 22 | 23 | public class UserController 24 | { 25 | public void RenameUser(int userId, string newName) 26 | { 27 | User user = GetUserFromDatabase(userId); 28 | user.Name = newName; 29 | SaveUserToDatabase(user); 30 | } 31 | 32 | private void SaveUserToDatabase(User user) 33 | { 34 | } 35 | 36 | private User GetUserFromDatabase(int userId) 37 | { 38 | return new User(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Book/Chapter5/Listing9/CustomerControllerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Moq; 4 | using Xunit; 5 | 6 | namespace Book.Chapter5.Listing9 7 | { 8 | public class CustomerControllerTests 9 | { 10 | [Fact(Skip = "Concept illustration only")] 11 | public void Successful_purchase() 12 | { 13 | var mock = new Mock(); 14 | var sut = new CustomerController(mock.Object); 15 | 16 | bool isSuccess = sut.Purchase( 17 | customerId: 1, productId: 2, quantity: 5); 18 | 19 | Assert.True(isSuccess); 20 | mock.Verify( 21 | x => x.SendReceipt( 22 | "customer@email.com", "Shampoo", 5), 23 | Times.Once); 24 | } 25 | } 26 | 27 | public class CustomerTests 28 | { 29 | [Fact] 30 | public void Purchase_succeeds_when_enough_inventory() 31 | { 32 | var storeMock = new Mock(); 33 | storeMock 34 | .Setup(x => x.HasEnoughInventory(Product.Shampoo, 5)) 35 | .Returns(true); 36 | var customer = new Customer(); 37 | 38 | bool success = customer.Purchase(storeMock.Object, Product.Shampoo, 5); 39 | 40 | Assert.True(success); 41 | storeMock.Verify( 42 | x => x.RemoveInventory(Product.Shampoo, 5), 43 | Times.Once); 44 | } 45 | } 46 | 47 | public class CustomerController 48 | { 49 | private readonly CustomerRepository _customerRepository; 50 | private readonly ProductRepository _productRepository; 51 | private readonly Store _mainStore; 52 | private readonly IEmailGateway _emailGateway; 53 | 54 | public CustomerController(IEmailGateway emailGateway) 55 | { 56 | _emailGateway = emailGateway; 57 | } 58 | 59 | public bool Purchase(int customerId, int productId, int quantity) 60 | { 61 | Customer customer = _customerRepository.GetById(customerId); 62 | Product product = _productRepository.GetById(productId); 63 | 64 | bool isSuccess = customer.Purchase(_mainStore, product, quantity); 65 | 66 | if (isSuccess) 67 | { 68 | _emailGateway.SendReceipt(customer.Email, product.Name, quantity); 69 | } 70 | 71 | return isSuccess; 72 | } 73 | } 74 | 75 | public class EmailGateway : IEmailGateway 76 | { 77 | public void SendReceipt(string email, string productName, int quantity) 78 | { 79 | } 80 | } 81 | 82 | public interface IEmailGateway 83 | { 84 | void SendReceipt(string email, string productName, int quantity); 85 | } 86 | 87 | internal class ProductRepository 88 | { 89 | public Product GetById(int productId) 90 | { 91 | return new Product(); 92 | } 93 | } 94 | 95 | internal class CustomerRepository 96 | { 97 | public Customer GetById(int customerId) 98 | { 99 | return new Customer(); 100 | } 101 | } 102 | 103 | public interface IStore 104 | { 105 | bool HasEnoughInventory(Product product, int quantity); 106 | void RemoveInventory(Product product, int quantity); 107 | void AddInventory(Product product, int quantity); 108 | int GetInventory(Product product); 109 | } 110 | 111 | public class Store : IStore 112 | { 113 | private readonly Dictionary _inventory = new Dictionary(); 114 | public int Id { get; set; } 115 | 116 | public bool HasEnoughInventory(Product product, int quantity) 117 | { 118 | return GetInventory(product) >= quantity; 119 | } 120 | 121 | public void RemoveInventory(Product product, int quantity) 122 | { 123 | if (!HasEnoughInventory(product, quantity)) 124 | { 125 | throw new Exception("Not enough inventory"); 126 | } 127 | 128 | _inventory[product] -= quantity; 129 | } 130 | 131 | public void AddInventory(Product product, int quantity) 132 | { 133 | if (_inventory.ContainsKey(product)) 134 | { 135 | _inventory[product] += quantity; 136 | } 137 | else 138 | { 139 | _inventory.Add(product, quantity); 140 | } 141 | } 142 | 143 | public int GetInventory(Product product) 144 | { 145 | bool productExists = _inventory.TryGetValue(product, out int remaining); 146 | return productExists ? remaining : 0; 147 | } 148 | } 149 | 150 | public class Product 151 | { 152 | public int Id { get; set; } 153 | public string Name { get; set; } 154 | public static Product Shampoo { get; set; } 155 | } 156 | 157 | public class Customer 158 | { 159 | public bool Purchase(IStore store, Product product, int quantity) 160 | { 161 | if (!store.HasEnoughInventory(product, quantity)) 162 | { 163 | return false; 164 | } 165 | 166 | store.RemoveInventory(product, quantity); 167 | 168 | return true; 169 | } 170 | 171 | public string Email { get; set; } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Book/Chapter6/Listing1/PriceEngine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace Book.Chapter6.Listing1 5 | { 6 | public class CustomerControllerTests 7 | { 8 | [Fact] 9 | public void Discount_of_two_products() 10 | { 11 | var product1 = new Product("Hand wash"); 12 | var product2 = new Product("Shampoo"); 13 | var sut = new PriceEngine(); 14 | 15 | decimal discount = sut.CalculateDiscount( 16 | product1, product2); 17 | 18 | Assert.Equal(0.02m, discount); 19 | } 20 | } 21 | 22 | public class PriceEngine 23 | { 24 | public decimal CalculateDiscount(params Product[] product) 25 | { 26 | decimal discount = product.Length * 0.01m; 27 | return Math.Min(discount, 0.2m); 28 | } 29 | } 30 | 31 | public class Product 32 | { 33 | private string _name; 34 | 35 | public Product(string name) 36 | { 37 | _name = name; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Book/Chapter6/Listing2/PriceEngine.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Xunit; 4 | 5 | namespace Book.Chapter6.Listing2 6 | { 7 | public class CustomerControllerTests 8 | { 9 | [Fact] 10 | public void Adding_a_product_to_an_order() 11 | { 12 | var product = new Product("Hand wash"); 13 | var sut = new Order(); 14 | 15 | sut.AddProduct(product); 16 | 17 | Assert.Equal(1, sut.Products.Count); 18 | Assert.Equal(product, sut.Products[0]); 19 | } 20 | } 21 | 22 | public class Order 23 | { 24 | private readonly List _products = new List(); 25 | public IReadOnlyList Products => _products.ToList(); 26 | 27 | public void AddProduct(Product product) 28 | { 29 | _products.Add(product); 30 | } 31 | } 32 | 33 | public class Product 34 | { 35 | private string _name; 36 | 37 | public Product(string name) 38 | { 39 | _name = name; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Book/Chapter6/Listing4_6/Listing4.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace Book.Chapter6.Listing4_6 8 | { 9 | public class CustomerControllerTests 10 | { 11 | [Fact] 12 | public void Adding_a_comment_to_an_article() 13 | { 14 | var sut = new Article(); 15 | var text = "Comment text"; 16 | var author = "John Doe"; 17 | var now = new DateTime(2019, 4, 1); 18 | 19 | sut.AddComment(text, author, now); 20 | 21 | Assert.Equal(1, sut.Comments.Count); 22 | Assert.Equal(text, sut.Comments[0].Text); 23 | Assert.Equal(author, sut.Comments[0].Author); 24 | Assert.Equal(now, sut.Comments[0].DateCreated); 25 | } 26 | 27 | [Fact] 28 | public void Adding_a_comment_to_an_article2() 29 | { 30 | var sut = new Article(); 31 | var text = "Comment text"; 32 | var author = "John Doe"; 33 | var now = new DateTime(2019, 4, 1); 34 | 35 | sut.AddComment(text, author, now); 36 | 37 | sut.ShouldContainNumberOfComments(1) 38 | .WithComment(text, author, now); 39 | } 40 | 41 | [Fact] 42 | public void Adding_a_comment_to_an_article3() 43 | { 44 | var sut = new Article(); 45 | var comment = new Comment( 46 | "Comment text", 47 | "John Doe", 48 | new DateTime(2019, 4, 1)); 49 | 50 | sut.AddComment(comment.Text, comment.Author, comment.DateCreated); 51 | 52 | sut.Comments.Should() 53 | .BeEquivalentTo(comment); 54 | } 55 | } 56 | 57 | public class Article 58 | { 59 | private readonly List _comments = new List(); 60 | 61 | public IReadOnlyList Comments => 62 | _comments.ToList(); 63 | 64 | public void AddComment(string text, string author, DateTime now) 65 | { 66 | _comments.Add(new Comment(text, author, now)); 67 | } 68 | 69 | public Article ShouldContainNumberOfComments(int i) 70 | { 71 | return this; 72 | } 73 | } 74 | 75 | public class Comment 76 | { 77 | public readonly string Text; 78 | public readonly string Author; 79 | public readonly DateTime DateCreated; 80 | 81 | public Comment(string text, string author, DateTime dateCreated) 82 | { 83 | Text = text; 84 | Author = author; 85 | DateCreated = dateCreated; 86 | } 87 | 88 | protected bool Equals(Comment other) 89 | { 90 | return string.Equals(Text, other.Text) 91 | && string.Equals(Author, other.Author) 92 | && DateCreated.Equals(other.DateCreated); 93 | } 94 | 95 | public override bool Equals(object obj) 96 | { 97 | if (ReferenceEquals(null, obj)) 98 | { 99 | return false; 100 | } 101 | 102 | if (ReferenceEquals(this, obj)) 103 | { 104 | return true; 105 | } 106 | 107 | if (obj.GetType() != GetType()) 108 | { 109 | return false; 110 | } 111 | 112 | return Equals((Comment)obj); 113 | } 114 | 115 | public override int GetHashCode() 116 | { 117 | unchecked 118 | { 119 | int hashCode = (Text != null ? Text.GetHashCode() : 0); 120 | hashCode = (hashCode * 397) ^ (Author != null ? Author.GetHashCode() : 0); 121 | hashCode = (hashCode * 397) ^ DateCreated.GetHashCode(); 122 | return hashCode; 123 | } 124 | } 125 | } 126 | 127 | public static class ArticleExtensions 128 | { 129 | public static Article ShouldContainNumberOfComments(this Article article, int commentCount) 130 | { 131 | Assert.Equal(1, article.Comments.Count); 132 | return article; 133 | } 134 | 135 | public static Article WithComment(this Article article, string text, string author, DateTime dateCreated) 136 | { 137 | Comment comment = article.Comments.SingleOrDefault(x => x.Text == text && x.Author == author && x.DateCreated == dateCreated); 138 | Assert.NotNull(comment); 139 | return article; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Book/Chapter6/Listing7_/Before/ArchitectureBefore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | namespace Book.Chapter6.Listing7_.Before 7 | { 8 | public class AuditManager 9 | { 10 | private readonly int _maxEntriesPerFile; 11 | private readonly string _directoryName; 12 | 13 | public AuditManager(int maxEntriesPerFile, string directoryName) 14 | { 15 | _maxEntriesPerFile = maxEntriesPerFile; 16 | _directoryName = directoryName; 17 | } 18 | 19 | public void AddRecord(string visitorName, DateTime timeOfVisit) 20 | { 21 | string[] filePaths = Directory.GetFiles(_directoryName); 22 | (int index, string path)[] sorted = SortByIndex(filePaths); 23 | 24 | string newRecord = visitorName + ';' + timeOfVisit.ToString("s"); 25 | 26 | if (sorted.Length == 0) 27 | { 28 | string newFile = Path.Combine(_directoryName, "audit_1.txt"); 29 | File.WriteAllText(newFile, newRecord); 30 | return; 31 | } 32 | 33 | (int currentFileIndex, string currentFilePath) = sorted.Last(); 34 | List lines = File.ReadAllLines(currentFilePath).ToList(); 35 | 36 | if (lines.Count < _maxEntriesPerFile) 37 | { 38 | lines.Add(newRecord); 39 | string newContent = string.Join("\r\n", lines); 40 | File.WriteAllText(currentFilePath, newContent); 41 | } 42 | else 43 | { 44 | int newIndex = currentFileIndex + 1; 45 | string newName = $"audit_{newIndex}.txt"; 46 | string newFile = Path.Combine(_directoryName, newName); 47 | File.WriteAllText(newFile, newRecord); 48 | } 49 | } 50 | 51 | private (int index, string path)[] SortByIndex(string[] files) 52 | { 53 | return files 54 | .Select(path => (index: GetIndex(path), path)) 55 | .OrderBy(x => x.index) 56 | .ToArray(); 57 | } 58 | 59 | private int GetIndex(string filePath) 60 | { 61 | // File name example: audit_1.txt 62 | string fileName = Path.GetFileNameWithoutExtension(filePath); 63 | return int.Parse(fileName.Split('_')[1]); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Book/Chapter6/Listing7_/Functional/ArchitectureFunctional.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using FluentAssertions; 6 | using Moq; 7 | using Xunit; 8 | 9 | namespace Book.Chapter6.Listing7_.Functional 10 | { 11 | public class AuditManager 12 | { 13 | private readonly int _maxEntriesPerFile; 14 | 15 | public AuditManager(int maxEntriesPerFile) 16 | { 17 | _maxEntriesPerFile = maxEntriesPerFile; 18 | } 19 | 20 | public FileUpdate AddRecord( 21 | FileContent[] files, 22 | string visitorName, 23 | DateTime timeOfVisit) 24 | { 25 | (int index, FileContent file)[] sorted = SortByIndex(files); 26 | 27 | string newRecord = visitorName + ';' + timeOfVisit.ToString("s"); 28 | 29 | if (sorted.Length == 0) 30 | { 31 | return new FileUpdate("audit_1.txt", newRecord); 32 | } 33 | 34 | (int currentFileIndex, FileContent currentFile) = sorted.Last(); 35 | List lines = currentFile.Lines.ToList(); 36 | 37 | if (lines.Count < _maxEntriesPerFile) 38 | { 39 | lines.Add(newRecord); 40 | string newContent = string.Join("\r\n", lines); 41 | return new FileUpdate(currentFile.FileName, newContent); 42 | } 43 | else 44 | { 45 | int newIndex = currentFileIndex + 1; 46 | string newName = $"audit_{newIndex}.txt"; 47 | return new FileUpdate(newName, newRecord); 48 | } 49 | } 50 | 51 | private (int index, FileContent file)[] SortByIndex( 52 | FileContent[] files) 53 | { 54 | return files 55 | .Select(file => (index: GetIndex(file.FileName), file)) 56 | .OrderBy(x => x.index) 57 | .ToArray(); 58 | } 59 | 60 | private int GetIndex(string fileName) 61 | { 62 | // File name example: audit_1.txt 63 | string name = Path.GetFileNameWithoutExtension(fileName); 64 | return int.Parse(name.Split('_')[1]); 65 | } 66 | } 67 | 68 | public struct FileUpdate 69 | { 70 | public readonly string FileName; 71 | public readonly string NewContent; 72 | 73 | public FileUpdate(string fileName, string newContent) 74 | { 75 | FileName = fileName; 76 | NewContent = newContent; 77 | } 78 | } 79 | 80 | public class FileContent 81 | { 82 | public readonly string FileName; 83 | public readonly string[] Lines; 84 | 85 | public FileContent(string fileName, string[] lines) 86 | { 87 | FileName = fileName; 88 | Lines = lines; 89 | } 90 | } 91 | 92 | public class Persister 93 | { 94 | public FileContent[] ReadDirectory(string directoryName) 95 | { 96 | return Directory 97 | .GetFiles(directoryName) 98 | .Select(x => new FileContent( 99 | Path.GetFileName(x), 100 | File.ReadAllLines(x))) 101 | .ToArray(); 102 | } 103 | 104 | public void ApplyUpdate(string directoryName, FileUpdate update) 105 | { 106 | string filePath = Path.Combine(directoryName, update.FileName); 107 | File.WriteAllText(filePath, update.NewContent); 108 | } 109 | } 110 | 111 | public class ApplicationService 112 | { 113 | private readonly string _directoryName; 114 | private readonly AuditManager _auditManager; 115 | private readonly Persister _persister; 116 | 117 | public ApplicationService(string directoryName, int maxEntriesPerFile) 118 | { 119 | _directoryName = directoryName; 120 | _auditManager = new AuditManager(maxEntriesPerFile); 121 | _persister = new Persister(); 122 | } 123 | 124 | public void AddRecord(string visitorName, DateTime timeOfVisit) 125 | { 126 | FileContent[] files = _persister.ReadDirectory(_directoryName); 127 | FileUpdate update = _auditManager.AddRecord( 128 | files, visitorName, timeOfVisit); 129 | _persister.ApplyUpdate(_directoryName, update); 130 | } 131 | } 132 | 133 | public class Tests 134 | { 135 | [Fact] 136 | public void A_new_file_is_created_when_the_current_file_overflows() 137 | { 138 | var sut = new AuditManager(3); 139 | var files = new FileContent[] 140 | { 141 | new FileContent("audit_1.txt", new string[0]), 142 | new FileContent("audit_2.txt", new string[] 143 | { 144 | "Peter; 2019-04-06T16:30:00", 145 | "Jane; 2019-04-06T16:40:00", 146 | "Jack; 2019-04-06T17:00:00" 147 | }) 148 | }; 149 | 150 | FileUpdate update = sut.AddRecord( 151 | files, "Alice", DateTime.Parse("2019-04-06T18:00:00")); 152 | 153 | Assert.Equal("audit_3.txt", update.FileName); 154 | Assert.Equal("Alice;2019-04-06T18:00:00", update.NewContent); 155 | Assert.Equal( 156 | new FileUpdate("audit_3.txt", "Alice;2019-04-06T18:00:00"), 157 | update); 158 | update.Should().Be( 159 | new FileUpdate("audit_3.txt", "Alice;2019-04-06T18:00:00")); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Book/Chapter6/Listing7_/Mocks/ArchitectureMocks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using Moq; 6 | using Xunit; 7 | 8 | namespace Book.Chapter6.Listing7_.Mocks 9 | { 10 | public class AuditManager 11 | { 12 | private readonly int _maxEntriesPerFile; 13 | private readonly string _directoryName; 14 | private readonly IFileSystem _fileSystem; 15 | 16 | public AuditManager( 17 | int maxEntriesPerFile, 18 | string directoryName, 19 | IFileSystem fileSystem) 20 | { 21 | _maxEntriesPerFile = maxEntriesPerFile; 22 | _directoryName = directoryName; 23 | _fileSystem = fileSystem; 24 | } 25 | 26 | public void AddRecord(string visitorName, DateTime timeOfVisit) 27 | { 28 | string[] filePaths = _fileSystem.GetFiles(_directoryName); 29 | (int index, string path)[] sorted = SortByIndex(filePaths); 30 | 31 | string newRecord = visitorName + ';' + timeOfVisit.ToString("s"); 32 | 33 | if (sorted.Length == 0) 34 | { 35 | string newFile = Path.Combine(_directoryName, "audit_1.txt"); 36 | _fileSystem.WriteAllText(newFile, newRecord); 37 | return; 38 | } 39 | 40 | (int currentFileIndex, string currentFilePath) = sorted.Last(); 41 | List lines = _fileSystem.ReadAllLines(currentFilePath); 42 | 43 | if (lines.Count < _maxEntriesPerFile) 44 | { 45 | lines.Add(newRecord); 46 | string newContent = string.Join("\r\n", lines); 47 | _fileSystem.WriteAllText(currentFilePath, newContent); 48 | } 49 | else 50 | { 51 | int newIndex = currentFileIndex + 1; 52 | string newName = $"audit_{newIndex}.txt"; 53 | string newFile = Path.Combine(_directoryName, newName); 54 | _fileSystem.WriteAllText(newFile, newRecord); 55 | } 56 | } 57 | 58 | private (int index, string path)[] SortByIndex(string[] files) 59 | { 60 | return files 61 | .Select(path => (index: GetIndex(path), path)) 62 | .OrderBy(x => x.index) 63 | .ToArray(); 64 | } 65 | 66 | private int GetIndex(string filePath) 67 | { 68 | // File name example: audit_1.txt 69 | string fileName = Path.GetFileNameWithoutExtension(filePath); 70 | return int.Parse(fileName.Split('_')[1]); 71 | } 72 | } 73 | 74 | public interface IFileSystem 75 | { 76 | string[] GetFiles(string directoryName); 77 | void WriteAllText(string filePath, string content); 78 | List ReadAllLines(string filePath); 79 | } 80 | 81 | public class Tests 82 | { 83 | [Fact] 84 | public void A_new_file_is_created_for_the_first_entry() 85 | { 86 | var fileSystemMock = new Mock(); 87 | fileSystemMock 88 | .Setup(x => x.GetFiles("audits")) 89 | .Returns(new string[0]); 90 | var sut = new AuditManager(3, "audits", fileSystemMock.Object); 91 | 92 | sut.AddRecord("Peter", DateTime.Parse("2019-04-09T13:00:00")); 93 | 94 | fileSystemMock.Verify(x => x.WriteAllText( 95 | @"audits\audit_1.txt", 96 | "Peter;2019-04-09T13:00:00")); 97 | } 98 | 99 | [Fact] 100 | public void A_new_file_is_created_when_the_current_file_overflows() 101 | { 102 | var fileSystemMock = new Mock(); 103 | fileSystemMock 104 | .Setup(x => x.GetFiles("audits")) 105 | .Returns(new string[] 106 | { 107 | @"audits\audit_1.txt", 108 | @"audits\audit_2.txt" 109 | }); 110 | fileSystemMock 111 | .Setup(x => x.ReadAllLines(@"audits\audit_2.txt")) 112 | .Returns(new List 113 | { 114 | "Peter; 2019-04-06T16:30:00", 115 | "Jane; 2019-04-06T16:40:00", 116 | "Jack; 2019-04-06T17:00:00" 117 | }); 118 | var sut = new AuditManager(3, "audits", fileSystemMock.Object); 119 | 120 | sut.AddRecord("Alice", DateTime.Parse("2019-04-06T18:00:00")); 121 | 122 | fileSystemMock.Verify(x => x.WriteAllText( 123 | @"audits\audit_3.txt", 124 | "Alice;2019-04-06T18:00:00")); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Book/Chapter7/CanExecute/CanExecute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace Book.Chapter7.CanExecute 5 | { 6 | public class User 7 | { 8 | public int UserId { get; private set; } 9 | public string Email { get; private set; } 10 | public UserType Type { get; private set; } 11 | public bool IsEmailConfirmed { get; private set; } 12 | 13 | public User(int userId, string email, UserType type, bool isEmailConfirmed) 14 | { 15 | UserId = userId; 16 | Email = email; 17 | Type = type; 18 | IsEmailConfirmed = isEmailConfirmed; 19 | } 20 | 21 | public string CanChangeEmail() 22 | { 23 | if (IsEmailConfirmed) 24 | return "Can't change email after it's confirmed"; 25 | 26 | return null; 27 | } 28 | 29 | public void ChangeEmail(string newEmail, Company company) 30 | { 31 | Precondition.Requires(CanChangeEmail() == null); 32 | 33 | if (Email == newEmail) 34 | return; 35 | 36 | UserType newType = company.IsEmailCorporate(newEmail) 37 | ? UserType.Employee 38 | : UserType.Customer; 39 | 40 | if (Type != newType) 41 | { 42 | int delta = newType == UserType.Employee ? 1 : -1; 43 | company.ChangeNumberOfEmployees(delta); 44 | } 45 | 46 | Email = newEmail; 47 | Type = newType; 48 | } 49 | } 50 | 51 | public class UserFactory 52 | { 53 | public static User Create(object[] data) 54 | { 55 | return null; 56 | } 57 | } 58 | 59 | public class UserController 60 | { 61 | private readonly Database _database = new Database(); 62 | private readonly MessageBus _messageBus = new MessageBus(); 63 | 64 | public string ChangeEmail(int userId, string newEmail) 65 | { 66 | object[] userData = _database.GetUserById(userId); 67 | User user = UserFactory.Create(userData); 68 | 69 | string error = user.CanChangeEmail(); 70 | if (error != null) 71 | return error; 72 | 73 | object[] companyData = _database.GetCompany(); 74 | Company company = CompanyFactory.Create(companyData); 75 | 76 | user.ChangeEmail(newEmail, company); 77 | 78 | _database.SaveCompany(company); 79 | _database.SaveUser(user); 80 | _messageBus.SendEmailChangedMessage(userId, newEmail); 81 | 82 | return "OK"; 83 | } 84 | } 85 | 86 | public class Company 87 | { 88 | public string DomainName { get; private set; } 89 | public int NumberOfEmployees { get; private set; } 90 | 91 | public Company(string domainName, int numberOfEmployees) 92 | { 93 | DomainName = domainName; 94 | NumberOfEmployees = numberOfEmployees; 95 | } 96 | 97 | public void ChangeNumberOfEmployees(int delta) 98 | { 99 | Precondition.Requires(NumberOfEmployees + delta >= 0); 100 | 101 | NumberOfEmployees += delta; 102 | } 103 | 104 | public bool IsEmailCorporate(string email) 105 | { 106 | string emailDomain = email.Split('@')[1]; 107 | return emailDomain == DomainName; 108 | } 109 | } 110 | 111 | public class CompanyFactory 112 | { 113 | public static Company Create(object[] data) 114 | { 115 | Precondition.Requires(data.Length >= 2); 116 | 117 | string domainName = (string)data[0]; 118 | int numberOfEmployees = (int)data[1]; 119 | 120 | return new Company(domainName, numberOfEmployees); 121 | } 122 | } 123 | 124 | public enum UserType 125 | { 126 | Customer = 1, 127 | Employee = 2 128 | } 129 | 130 | public static class Precondition 131 | { 132 | public static void Requires(bool precondition, string message = null) 133 | { 134 | if (precondition == false) 135 | throw new Exception(message); 136 | } 137 | } 138 | 139 | public class Database 140 | { 141 | public object[] GetUserById(int userId) 142 | { 143 | return null; 144 | } 145 | 146 | public User GetUserByEmail(string email) 147 | { 148 | return null; 149 | } 150 | 151 | public void SaveUser(User user) 152 | { 153 | } 154 | 155 | public object[] GetCompany() 156 | { 157 | return null; 158 | } 159 | 160 | public void SaveCompany(Company company) 161 | { 162 | } 163 | } 164 | 165 | public class MessageBus 166 | { 167 | private IBus _bus; 168 | 169 | public void SendEmailChangedMessage(int userId, string newEmail) 170 | { 171 | _bus.Send($"Subject: USER; Type: EMAIL CHANGED; Id: {userId}; NewEmail: {newEmail}"); 172 | } 173 | } 174 | 175 | internal interface IBus 176 | { 177 | void Send(string message); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Book/Chapter7/DomainEvents/DomainEvents.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace Book.Chapter7.DomainEvents 8 | { 9 | public class User 10 | { 11 | public int UserId { get; private set; } 12 | public string Email { get; private set; } 13 | public UserType Type { get; private set; } 14 | public bool IsEmailConfirmed { get; private set; } 15 | public List EmailChangedEvents { get; private set; } 16 | 17 | public User(int userId, string email, UserType type, bool isEmailConfirmed) 18 | { 19 | UserId = userId; 20 | Email = email; 21 | Type = type; 22 | IsEmailConfirmed = isEmailConfirmed; 23 | EmailChangedEvents = new List(); 24 | } 25 | 26 | public string CanChangeEmail() 27 | { 28 | if (IsEmailConfirmed) 29 | return "Can't change email after it's confirmed"; 30 | 31 | return null; 32 | } 33 | 34 | public void ChangeEmail(string newEmail, Company company) 35 | { 36 | Precondition.Requires(CanChangeEmail() == null); 37 | 38 | if (Email == newEmail) 39 | return; 40 | 41 | UserType newType = company.IsEmailCorporate(newEmail) 42 | ? UserType.Employee 43 | : UserType.Customer; 44 | 45 | if (Type != newType) 46 | { 47 | int delta = newType == UserType.Employee ? 1 : -1; 48 | company.ChangeNumberOfEmployees(delta); 49 | } 50 | 51 | Email = newEmail; 52 | Type = newType; 53 | EmailChangedEvents.Add(new EmailChangedEvent(UserId, newEmail)); 54 | } 55 | } 56 | 57 | public class UserController 58 | { 59 | private readonly Database _database = new Database(); 60 | private readonly MessageBus _messageBus = new MessageBus(); 61 | 62 | public string ChangeEmail(int userId, string newEmail) 63 | { 64 | object[] userData = _database.GetUserById(userId); 65 | User user = UserFactory.Create(userData); 66 | 67 | string error = user.CanChangeEmail(); 68 | if (error != null) 69 | return error; 70 | 71 | object[] companyData = _database.GetCompany(); 72 | Company company = CompanyFactory.Create(companyData); 73 | 74 | user.ChangeEmail(newEmail, company); 75 | 76 | _database.SaveCompany(company); 77 | _database.SaveUser(user); 78 | foreach (EmailChangedEvent ev in user.EmailChangedEvents) 79 | { 80 | _messageBus.SendEmailChangedMessage(ev.UserId, ev.NewEmail); 81 | } 82 | 83 | return "OK"; 84 | } 85 | } 86 | 87 | public class EmailChangedEvent 88 | { 89 | public int UserId { get; } 90 | public string NewEmail { get; } 91 | 92 | public EmailChangedEvent(int userId, string newEmail) 93 | { 94 | UserId = userId; 95 | NewEmail = newEmail; 96 | } 97 | 98 | protected bool Equals(EmailChangedEvent other) 99 | { 100 | return UserId == other.UserId && string.Equals(NewEmail, other.NewEmail); 101 | } 102 | 103 | public override bool Equals(object obj) 104 | { 105 | if (ReferenceEquals(null, obj)) 106 | { 107 | return false; 108 | } 109 | 110 | if (ReferenceEquals(this, obj)) 111 | { 112 | return true; 113 | } 114 | 115 | if (obj.GetType() != this.GetType()) 116 | { 117 | return false; 118 | } 119 | 120 | return Equals((EmailChangedEvent)obj); 121 | } 122 | 123 | public override int GetHashCode() 124 | { 125 | unchecked 126 | { 127 | return (UserId * 397) ^ (NewEmail != null ? NewEmail.GetHashCode() : 0); 128 | } 129 | } 130 | } 131 | 132 | public class UserFactory 133 | { 134 | public static User Create(object[] data) 135 | { 136 | return null; 137 | } 138 | } 139 | 140 | public class Company 141 | { 142 | public string DomainName { get; private set; } 143 | public int NumberOfEmployees { get; private set; } 144 | 145 | public Company(string domainName, int numberOfEmployees) 146 | { 147 | DomainName = domainName; 148 | NumberOfEmployees = numberOfEmployees; 149 | } 150 | 151 | public void ChangeNumberOfEmployees(int delta) 152 | { 153 | Precondition.Requires(NumberOfEmployees + delta >= 0); 154 | 155 | NumberOfEmployees += delta; 156 | } 157 | 158 | public bool IsEmailCorporate(string email) 159 | { 160 | string emailDomain = email.Split('@')[1]; 161 | return emailDomain == DomainName; 162 | } 163 | } 164 | 165 | public class CompanyFactory 166 | { 167 | public static Company Create(object[] data) 168 | { 169 | Precondition.Requires(data.Length >= 2); 170 | 171 | string domainName = (string)data[0]; 172 | int numberOfEmployees = (int)data[1]; 173 | 174 | return new Company(domainName, numberOfEmployees); 175 | } 176 | } 177 | 178 | public enum UserType 179 | { 180 | Customer = 1, 181 | Employee = 2 182 | } 183 | 184 | public class Tests 185 | { 186 | [Fact] 187 | public void Changing_email_from_corporate_to_non_corporate() 188 | { 189 | var company = new Company("mycorp.com", 1); 190 | var sut = new User(1, "user@mycorp.com", UserType.Employee, false); 191 | 192 | sut.ChangeEmail("new@gmail.com", company); 193 | 194 | company.NumberOfEmployees.Should().Be(0); 195 | sut.Email.Should().Be("new@gmail.com"); 196 | sut.Type.Should().Be(UserType.Customer); 197 | sut.EmailChangedEvents.Should().Equal( 198 | new EmailChangedEvent(1, "new@gmail.com")); 199 | } 200 | } 201 | 202 | public static class Precondition 203 | { 204 | public static void Requires(bool precondition, string message = null) 205 | { 206 | if (precondition == false) 207 | throw new Exception(message); 208 | } 209 | } 210 | 211 | public class Database 212 | { 213 | public object[] GetUserById(int userId) 214 | { 215 | return null; 216 | } 217 | 218 | public User GetUserByEmail(string email) 219 | { 220 | return null; 221 | } 222 | 223 | public void SaveUser(User user) 224 | { 225 | } 226 | 227 | public object[] GetCompany() 228 | { 229 | return null; 230 | } 231 | 232 | public void SaveCompany(Company company) 233 | { 234 | } 235 | } 236 | 237 | public class MessageBus 238 | { 239 | private IBus _bus; 240 | 241 | public void SendEmailChangedMessage(int userId, string newEmail) 242 | { 243 | _bus.Send($"Subject: USER; Type: EMAIL CHANGED; Id: {userId}; NewEmail: {newEmail}"); 244 | } 245 | } 246 | 247 | internal interface IBus 248 | { 249 | void Send(string message); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /Book/Chapter7/Refactored_1/Refactored_1.cs: -------------------------------------------------------------------------------- 1 | namespace Book.Chapter7.Refactored_1 2 | { 3 | public class User 4 | { 5 | public int UserId { get; private set; } 6 | public string Email { get; private set; } 7 | public UserType Type { get; private set; } 8 | 9 | public User(int userId, string email, UserType type) 10 | { 11 | UserId = userId; 12 | Email = email; 13 | Type = type; 14 | } 15 | 16 | public int ChangeEmail(string newEmail, 17 | string companyDomainName, int numberOfEmployees) 18 | { 19 | if (Email == newEmail) 20 | return numberOfEmployees; 21 | 22 | string emailDomain = newEmail.Split('@')[1]; 23 | bool isEmailCorporate = emailDomain == companyDomainName; 24 | UserType newType = isEmailCorporate 25 | ? UserType.Employee 26 | : UserType.Customer; 27 | 28 | if (Type != newType) 29 | { 30 | int delta = newType == UserType.Employee ? 1 : -1; 31 | int newNumber = numberOfEmployees + delta; 32 | numberOfEmployees = newNumber; 33 | } 34 | 35 | Email = newEmail; 36 | Type = newType; 37 | 38 | return numberOfEmployees; 39 | } 40 | } 41 | 42 | public class UserController 43 | { 44 | private readonly Database _database = new Database(); 45 | private readonly MessageBus _messageBus = new MessageBus(); 46 | 47 | public void ChangeEmail(int userId, string newEmail) 48 | { 49 | object[] data = _database.GetUserById(userId); 50 | string email = (string)data[1]; 51 | UserType type = (UserType)data[2]; 52 | var user = new User(userId, email, type); 53 | 54 | object[] companyData = _database.GetCompany(); 55 | string companyDomainName = (string)companyData[0]; 56 | int numberOfEmployees = (int)companyData[1]; 57 | 58 | int newNumberOfEmployees = user.ChangeEmail( 59 | newEmail, companyDomainName, numberOfEmployees); 60 | 61 | _database.SaveCompany(newNumberOfEmployees); 62 | _database.SaveUser(user); 63 | _messageBus.SendEmailChangedMessage(userId, newEmail); 64 | } 65 | } 66 | 67 | public enum UserType 68 | { 69 | Customer = 1, 70 | Employee = 2 71 | } 72 | 73 | public class Database 74 | { 75 | public object[] GetUserById(int userId) 76 | { 77 | return null; 78 | } 79 | 80 | public User GetUserByEmail(string email) 81 | { 82 | return null; 83 | } 84 | 85 | public void SaveUser(User user) 86 | { 87 | } 88 | 89 | public object[] GetCompany() 90 | { 91 | return null; 92 | } 93 | 94 | public void SaveCompany(int newNumber) 95 | { 96 | } 97 | } 98 | 99 | public class MessageBus 100 | { 101 | private IBus _bus; 102 | 103 | public void SendEmailChangedMessage(int userId, string newEmail) 104 | { 105 | _bus.Send($"Subject: USER; Type: EMAIL CHANGED; Id: {userId}; NewEmail: {newEmail}"); 106 | } 107 | } 108 | 109 | internal interface IBus 110 | { 111 | void Send(string message); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Book/Chapter7/Refactored_2/Refactored_2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Book.Chapter7.Refactored_2 4 | { 5 | public class User 6 | { 7 | public int UserId { get; private set; } 8 | public string Email { get; private set; } 9 | public UserType Type { get; private set; } 10 | 11 | public User(int userId, string email, UserType type) 12 | { 13 | UserId = userId; 14 | Email = email; 15 | Type = type; 16 | } 17 | 18 | public int ChangeEmail(string newEmail, 19 | string companyDomainName, int numberOfEmployees) 20 | { 21 | if (Email == newEmail) 22 | return numberOfEmployees; 23 | 24 | string emailDomain = newEmail.Split('@')[1]; 25 | bool isEmailCorporate = emailDomain == companyDomainName; 26 | UserType newType = isEmailCorporate 27 | ? UserType.Employee 28 | : UserType.Customer; 29 | 30 | if (Type != newType) 31 | { 32 | int delta = newType == UserType.Employee ? 1 : -1; 33 | int newNumber = numberOfEmployees + delta; 34 | numberOfEmployees = newNumber; 35 | } 36 | 37 | Email = newEmail; 38 | Type = newType; 39 | 40 | return numberOfEmployees; 41 | } 42 | } 43 | 44 | public class UserFactory 45 | { 46 | public static User Create(object[] data) 47 | { 48 | Precondition.Requires(data.Length >= 3); 49 | 50 | int id = (int)data[0]; 51 | string email = (string)data[1]; 52 | UserType type = (UserType)data[2]; 53 | 54 | return new User(id, email, type); 55 | } 56 | } 57 | 58 | public class UserController 59 | { 60 | private readonly Database _database = new Database(); 61 | private readonly MessageBus _messageBus = new MessageBus(); 62 | 63 | public void ChangeEmail(int userId, string newEmail) 64 | { 65 | object[] userData = _database.GetUserById(userId); 66 | User user = UserFactory.Create(userData); 67 | 68 | object[] companyData = _database.GetCompany(); 69 | string companyDomainName = (string)companyData[0]; 70 | int numberOfEmployees = (int)companyData[1]; 71 | 72 | int newNumberOfEmployees = user.ChangeEmail( 73 | newEmail, companyDomainName, numberOfEmployees); 74 | 75 | _database.SaveCompany(newNumberOfEmployees); 76 | _database.SaveUser(user); 77 | _messageBus.SendEmailChangedMessage(userId, newEmail); 78 | } 79 | } 80 | 81 | public enum UserType 82 | { 83 | Customer = 1, 84 | Employee = 2 85 | } 86 | 87 | public static class Precondition 88 | { 89 | public static void Requires(bool precondition, string message = null) 90 | { 91 | if (precondition == false) 92 | throw new Exception(message); 93 | } 94 | } 95 | 96 | public class Database 97 | { 98 | public object[] GetUserById(int userId) 99 | { 100 | return null; 101 | } 102 | 103 | public User GetUserByEmail(string email) 104 | { 105 | return null; 106 | } 107 | 108 | public void SaveUser(User user) 109 | { 110 | } 111 | 112 | public object[] GetCompany() 113 | { 114 | return null; 115 | } 116 | 117 | public void SaveCompany(int newNumber) 118 | { 119 | } 120 | } 121 | 122 | public class MessageBus 123 | { 124 | private IBus _bus; 125 | 126 | public void SendEmailChangedMessage(int userId, string newEmail) 127 | { 128 | _bus.Send($"Subject: USER; Type: EMAIL CHANGED; Id: {userId}; NewEmail: {newEmail}"); 129 | } 130 | } 131 | 132 | internal interface IBus 133 | { 134 | void Send(string message); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Book/Chapter7/Refactored_3/Refactored_3.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace Book.Chapter7.Refactored_3 5 | { 6 | public class User 7 | { 8 | public int UserId { get; private set; } 9 | public string Email { get; private set; } 10 | public UserType Type { get; private set; } 11 | 12 | public User(int userId, string email, UserType type) 13 | { 14 | UserId = userId; 15 | Email = email; 16 | Type = type; 17 | } 18 | 19 | public void ChangeEmail(string newEmail, Company company) 20 | { 21 | if (Email == newEmail) 22 | return; 23 | 24 | UserType newType = company.IsEmailCorporate(newEmail) 25 | ? UserType.Employee 26 | : UserType.Customer; 27 | 28 | if (Type != newType) 29 | { 30 | int delta = newType == UserType.Employee ? 1 : -1; 31 | company.ChangeNumberOfEmployees(delta); 32 | } 33 | 34 | Email = newEmail; 35 | Type = newType; 36 | } 37 | } 38 | 39 | public class UserFactory 40 | { 41 | public static User Create(object[] data) 42 | { 43 | Precondition.Requires(data.Length >= 3); 44 | 45 | int id = (int)data[0]; 46 | string email = (string)data[1]; 47 | UserType type = (UserType)data[2]; 48 | 49 | return new User(id, email, type); 50 | } 51 | } 52 | 53 | public class UserController 54 | { 55 | private readonly Database _database = new Database(); 56 | private readonly MessageBus _messageBus = new MessageBus(); 57 | 58 | public void ChangeEmail(int userId, string newEmail) 59 | { 60 | object[] userData = _database.GetUserById(userId); 61 | User user = UserFactory.Create(userData); 62 | 63 | object[] companyData = _database.GetCompany(); 64 | Company company = CompanyFactory.Create(companyData); 65 | 66 | user.ChangeEmail(newEmail, company); 67 | 68 | _database.SaveCompany(company); 69 | _database.SaveUser(user); 70 | _messageBus.SendEmailChangedMessage(userId, newEmail); 71 | } 72 | } 73 | 74 | public class Company 75 | { 76 | public string DomainName { get; private set; } 77 | public int NumberOfEmployees { get; private set; } 78 | 79 | public Company(string domainName, int numberOfEmployees) 80 | { 81 | DomainName = domainName; 82 | NumberOfEmployees = numberOfEmployees; 83 | } 84 | 85 | public void ChangeNumberOfEmployees(int delta) 86 | { 87 | Precondition.Requires(NumberOfEmployees + delta >= 0); 88 | 89 | NumberOfEmployees += delta; 90 | } 91 | 92 | public bool IsEmailCorporate(string email) 93 | { 94 | string emailDomain = email.Split('@')[1]; 95 | return emailDomain == DomainName; 96 | } 97 | } 98 | 99 | public class CompanyFactory 100 | { 101 | public static Company Create(object[] data) 102 | { 103 | Precondition.Requires(data.Length >= 2); 104 | 105 | string domainName = (string)data[0]; 106 | int numberOfEmployees = (int)data[1]; 107 | 108 | return new Company(domainName, numberOfEmployees); 109 | } 110 | } 111 | 112 | public enum UserType 113 | { 114 | Customer = 1, 115 | Employee = 2 116 | } 117 | 118 | public class Tests 119 | { 120 | [Fact] 121 | public void Changing_email_without_changing_user_type() 122 | { 123 | var company = new Company("mycorp.com", 1); 124 | var sut = new User(1, "user@mycorp.com", UserType.Employee); 125 | 126 | sut.ChangeEmail("new@mycorp.com", company); 127 | 128 | Assert.Equal(1, company.NumberOfEmployees); 129 | Assert.Equal("new@mycorp.com", sut.Email); 130 | Assert.Equal(UserType.Employee, sut.Type); 131 | } 132 | 133 | [Fact] 134 | public void Changing_email_from_corporate_to_non_corporate() 135 | { 136 | var company = new Company("mycorp.com", 1); 137 | var sut = new User(1, "user@mycorp.com", UserType.Employee); 138 | 139 | sut.ChangeEmail("new@gmail.com", company); 140 | 141 | Assert.Equal(0, company.NumberOfEmployees); 142 | Assert.Equal("new@gmail.com", sut.Email); 143 | Assert.Equal(UserType.Customer, sut.Type); 144 | } 145 | 146 | [Fact] 147 | public void Changing_email_from_non_corporate_to_corporate() 148 | { 149 | var company = new Company("mycorp.com", 1); 150 | var sut = new User(1, "user@gmail.com", UserType.Customer); 151 | 152 | sut.ChangeEmail("new@mycorp.com", company); 153 | 154 | Assert.Equal(2, company.NumberOfEmployees); 155 | Assert.Equal("new@mycorp.com", sut.Email); 156 | Assert.Equal(UserType.Employee, sut.Type); 157 | } 158 | 159 | [Fact] 160 | public void Changing_email_to_the_same_one() 161 | { 162 | var company = new Company("mycorp.com", 1); 163 | var sut = new User(1, "user@gmail.com", UserType.Customer); 164 | 165 | sut.ChangeEmail("user@gmail.com", company); 166 | 167 | Assert.Equal(1, company.NumberOfEmployees); 168 | Assert.Equal("user@gmail.com", sut.Email); 169 | Assert.Equal(UserType.Customer, sut.Type); 170 | } 171 | 172 | [InlineData("mycorp.com", "email@mycorp.com", true)] 173 | [InlineData("mycorp.com", "email@gmail.com", false)] 174 | [Theory] 175 | public void Differentiates_a_corporate_email_from_non_corporate( 176 | string domain, string email, bool expectedResult) 177 | { 178 | var sut = new Company(domain, 0); 179 | 180 | bool isEmailCorporate = sut.IsEmailCorporate(email); 181 | 182 | Assert.Equal(expectedResult, isEmailCorporate); 183 | } 184 | } 185 | 186 | public static class Precondition 187 | { 188 | public static void Requires(bool precondition, string message = null) 189 | { 190 | if (precondition == false) 191 | throw new Exception(message); 192 | } 193 | } 194 | 195 | public class Database 196 | { 197 | public object[] GetUserById(int userId) 198 | { 199 | return null; 200 | } 201 | 202 | public User GetUserByEmail(string email) 203 | { 204 | return null; 205 | } 206 | 207 | public void SaveUser(User user) 208 | { 209 | } 210 | 211 | public object[] GetCompany() 212 | { 213 | return null; 214 | } 215 | 216 | public void SaveCompany(Company company) 217 | { 218 | } 219 | } 220 | 221 | public class MessageBus 222 | { 223 | private IBus _bus; 224 | 225 | public void SendEmailChangedMessage(int userId, string newEmail) 226 | { 227 | _bus.Send($"Subject: USER; Type: EMAIL CHANGED; Id: {userId}; NewEmail: {newEmail}"); 228 | } 229 | } 230 | 231 | internal interface IBus 232 | { 233 | void Send(string message); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /Book/Chapter7/SampleProject/SampleProject.cs: -------------------------------------------------------------------------------- 1 | namespace Book.Chapter7.SampleProject 2 | { 3 | public class User 4 | { 5 | public int UserId { get; private set; } 6 | public string Email { get; private set; } 7 | public UserType Type { get; private set; } 8 | 9 | public void ChangeEmail(int userId, string newEmail) 10 | { 11 | object[] data = Database.GetUserById(userId); 12 | UserId = userId; 13 | Email = (string)data[1]; 14 | Type = (UserType)data[2]; 15 | 16 | if (Email == newEmail) 17 | return; 18 | 19 | //bool isEmailTaken = Database.GetUserByEmail(newEmail) != null; 20 | //if (isEmailTaken) 21 | // return "Email is taken"; 22 | 23 | object[] companyData = Database.GetCompany(); 24 | string companyDomainName = (string)companyData[0]; 25 | int numberOfEmployees = (int)companyData[1]; 26 | 27 | string emailDomain = newEmail.Split('@')[1]; 28 | bool isEmailCorporate = emailDomain == companyDomainName; 29 | UserType newType = isEmailCorporate 30 | ? UserType.Employee 31 | : UserType.Customer; 32 | 33 | if (Type != newType) 34 | { 35 | int delta = newType == UserType.Employee ? 1 : -1; 36 | int newNumber = numberOfEmployees + delta; 37 | Database.SaveCompany(newNumber); 38 | } 39 | 40 | Email = newEmail; 41 | Type = newType; 42 | 43 | Database.SaveUser(this); 44 | MessageBus.SendEmailChangedMessage(UserId, newEmail); 45 | } 46 | } 47 | 48 | public enum UserType 49 | { 50 | Customer = 1, 51 | Employee = 2 52 | } 53 | 54 | public class Database 55 | { 56 | public static object[] GetUserById(int userId) 57 | { 58 | return null; 59 | } 60 | 61 | public static User GetUserByEmail(string email) 62 | { 63 | return null; 64 | } 65 | 66 | public static void SaveUser(User user) 67 | { 68 | } 69 | 70 | public static object[] GetCompany() 71 | { 72 | return null; 73 | } 74 | 75 | public static void SaveCompany(int newNumber) 76 | { 77 | } 78 | } 79 | 80 | public class MessageBus 81 | { 82 | private static IBus _bus; 83 | 84 | public static void SendEmailChangedMessage(int userId, string newEmail) 85 | { 86 | _bus.Send($"Subject: USER; Type: EMAIL CHANGED; Id: {userId}; NewEmail: {newEmail}"); 87 | } 88 | } 89 | 90 | internal interface IBus 91 | { 92 | void Send(string message); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Book/Chapter8/Circular/Circular.cs: -------------------------------------------------------------------------------- 1 | namespace Book.Chapter8.Circular 2 | { 3 | public class CheckOutService 4 | { 5 | public void CheckOut(int orderId) 6 | { 7 | var service = new ReportGenerationService(); 8 | service.GenerateReport(orderId, this); 9 | 10 | /* other work */ 11 | } 12 | } 13 | 14 | public class ReportGenerationService 15 | { 16 | public void GenerateReport( 17 | int orderId, 18 | CheckOutService checkOutService) 19 | { 20 | /* calls checkOutService when generation is completed */ 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Book/Chapter8/DI/DI.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Book.Chapter8.DI 5 | { 6 | public class User 7 | { 8 | private static readonly ILogger _logger = 9 | LogManager.GetLogger(typeof(User)); 10 | 11 | private IDomainLogger _domainLogger; 12 | public int UserId { get; set; } 13 | public string Email { get; private set; } 14 | public UserType Type { get; private set; } 15 | public bool IsEmailConfirmed { get; } 16 | public List DomainEvents { get; } 17 | 18 | public User(int userId, string email, UserType type, bool isEmailConfirmed) 19 | { 20 | UserId = userId; 21 | Email = email; 22 | Type = type; 23 | IsEmailConfirmed = isEmailConfirmed; 24 | DomainEvents = new List(); 25 | } 26 | 27 | public string CanChangeEmail() 28 | { 29 | if (IsEmailConfirmed) 30 | return "Can't change email after it's confirmed"; 31 | 32 | return null; 33 | } 34 | 35 | public void ChangeEmail( 36 | string newEmail, Company company, ILogger logger) 37 | { 38 | logger.Info( 39 | $"Changing email for user {UserId} to {newEmail}"); 40 | 41 | Precondition.Requires(CanChangeEmail() == null); 42 | 43 | if (Email == newEmail) 44 | return; 45 | 46 | UserType newType = company.IsEmailCorporate(newEmail) 47 | ? UserType.Employee 48 | : UserType.Customer; 49 | 50 | if (Type != newType) 51 | { 52 | int delta = newType == UserType.Employee ? 1 : -1; 53 | company.ChangeNumberOfEmployees(delta); 54 | AddDomainEvent( 55 | new UserTypeChangedEvent(UserId, Type, newType)); 56 | } 57 | 58 | Email = newEmail; 59 | Type = newType; 60 | AddDomainEvent(new EmailChangedEvent(UserId, newEmail)); 61 | 62 | logger.Info($"Email is changed for user {UserId}"); 63 | } 64 | 65 | private void AddDomainEvent(EmailChangedEvent userTypeChangedEvent) 66 | { 67 | } 68 | 69 | private void AddDomainEvent(UserTypeChangedEvent userTypeChangedEvent) 70 | { 71 | 72 | } 73 | } 74 | 75 | internal class LogManager 76 | { 77 | public static ILogger GetLogger(Type type) 78 | { 79 | throw new NotImplementedException(); 80 | } 81 | } 82 | 83 | public class UserController 84 | { 85 | private readonly Database _database = new Database(); 86 | private readonly MessageBus _messageBus = new MessageBus(); 87 | 88 | public string ChangeEmail(int userId, string newEmail) 89 | { 90 | object[] userData = _database.GetUserById(userId); 91 | User user = UserFactory.Create(userData); 92 | 93 | string error = user.CanChangeEmail(); 94 | if (error != null) 95 | return error; 96 | 97 | object[] companyData = _database.GetCompany(); 98 | Company company = CompanyFactory.Create(companyData); 99 | 100 | user.ChangeEmail(newEmail, company, null); 101 | 102 | _database.SaveCompany(company); 103 | _database.SaveUser(user); 104 | EventDispatcher.Dispatch(user.DomainEvents); 105 | 106 | return "OK"; 107 | } 108 | } 109 | 110 | public class EventDispatcher 111 | { 112 | public static void Dispatch(List userDomainEvents) 113 | { 114 | //foreach (EmailChangedEvent ev in user.DomainEvents) 115 | //{ 116 | // _messageBus.SendEmailChangedMessage(ev.UserId, ev.NewEmail); 117 | //} 118 | } 119 | } 120 | 121 | public class UserFactory 122 | { 123 | public static User Create(object[] data) 124 | { 125 | return null; 126 | } 127 | } 128 | 129 | public class CompanyFactory 130 | { 131 | public static Company Create(object[] data) 132 | { 133 | Precondition.Requires(data.Length >= 2); 134 | 135 | string domainName = (string)data[0]; 136 | int numberOfEmployees = (int)data[1]; 137 | 138 | return new Company(domainName, numberOfEmployees); 139 | } 140 | } 141 | 142 | internal interface IDomainLogger 143 | { 144 | void UserTypeHasChanged(int userId, UserType oldType, UserType newType); 145 | } 146 | 147 | public class DomainLogger : IDomainLogger 148 | { 149 | private readonly ILogger _logger; 150 | 151 | public DomainLogger(ILogger logger) 152 | { 153 | _logger = logger; 154 | } 155 | 156 | public void UserTypeHasChanged( 157 | int userId, UserType oldType, UserType newType) 158 | { 159 | _logger.Info( 160 | $"User {userId} changed type " + 161 | $"from {oldType} to {newType}"); 162 | } 163 | } 164 | 165 | public interface ILogger 166 | { 167 | void Info(string s); 168 | } 169 | 170 | public class UserTypeChangedEvent 171 | { 172 | public int UserId { get; } 173 | public UserType OldType { get; } 174 | public UserType NewType { get; } 175 | 176 | public UserTypeChangedEvent(int userId, UserType oldType, UserType newType) 177 | { 178 | UserId = userId; 179 | OldType = oldType; 180 | NewType = newType; 181 | } 182 | 183 | protected bool Equals(UserTypeChangedEvent other) 184 | { 185 | return UserId == other.UserId && string.Equals(OldType, other.OldType); 186 | } 187 | 188 | public override bool Equals(object obj) 189 | { 190 | if (ReferenceEquals(null, obj)) 191 | { 192 | return false; 193 | } 194 | 195 | if (ReferenceEquals(this, obj)) 196 | { 197 | return true; 198 | } 199 | 200 | if (obj.GetType() != this.GetType()) 201 | { 202 | return false; 203 | } 204 | 205 | return Equals((EmailChangedEvent)obj); 206 | } 207 | 208 | public override int GetHashCode() 209 | { 210 | unchecked 211 | { 212 | return (UserId * 397) ^ OldType.GetHashCode(); 213 | } 214 | } 215 | } 216 | 217 | public class EmailChangedEvent 218 | { 219 | public int UserId { get; } 220 | public string NewEmail { get; } 221 | 222 | public EmailChangedEvent(int userId, string newEmail) 223 | { 224 | UserId = userId; 225 | NewEmail = newEmail; 226 | } 227 | 228 | protected bool Equals(EmailChangedEvent other) 229 | { 230 | return UserId == other.UserId && string.Equals(NewEmail, other.NewEmail); 231 | } 232 | 233 | public override bool Equals(object obj) 234 | { 235 | if (ReferenceEquals(null, obj)) 236 | { 237 | return false; 238 | } 239 | 240 | if (ReferenceEquals(this, obj)) 241 | { 242 | return true; 243 | } 244 | 245 | if (obj.GetType() != this.GetType()) 246 | { 247 | return false; 248 | } 249 | 250 | return Equals((EmailChangedEvent)obj); 251 | } 252 | 253 | public override int GetHashCode() 254 | { 255 | unchecked 256 | { 257 | return (UserId * 397) ^ (NewEmail != null ? NewEmail.GetHashCode() : 0); 258 | } 259 | } 260 | } 261 | 262 | public class Company 263 | { 264 | public string DomainName { get; } 265 | public int NumberOfEmployees { get; private set; } 266 | 267 | public Company(string domainName, int numberOfEmployees) 268 | { 269 | DomainName = domainName; 270 | NumberOfEmployees = numberOfEmployees; 271 | } 272 | 273 | public void ChangeNumberOfEmployees(int delta) 274 | { 275 | Precondition.Requires(NumberOfEmployees + delta >= 0); 276 | 277 | NumberOfEmployees += delta; 278 | } 279 | 280 | public bool IsEmailCorporate(string email) 281 | { 282 | string emailDomain = email.Split('@')[1]; 283 | return emailDomain == DomainName; 284 | } 285 | } 286 | 287 | public enum UserType 288 | { 289 | Customer = 1, 290 | Employee = 2 291 | } 292 | 293 | public static class Precondition 294 | { 295 | public static void Requires(bool precondition, string message = null) 296 | { 297 | if (precondition == false) 298 | throw new Exception(message); 299 | } 300 | } 301 | 302 | public class Database 303 | { 304 | public object[] GetUserById(int userId) 305 | { 306 | return null; 307 | } 308 | 309 | public User GetUserByEmail(string email) 310 | { 311 | return null; 312 | } 313 | 314 | public void SaveUser(User user) 315 | { 316 | } 317 | 318 | public object[] GetCompany() 319 | { 320 | return null; 321 | } 322 | 323 | public void SaveCompany(Company company) 324 | { 325 | } 326 | } 327 | 328 | public class MessageBus 329 | { 330 | private IBus _bus; 331 | 332 | public void SendEmailChangedMessage(int userId, string newEmail) 333 | { 334 | _bus.Send($"Subject: USER; Type: EMAIL CHANGED; Id: {userId}; NewEmail: {newEmail}"); 335 | } 336 | } 337 | 338 | internal interface IBus 339 | { 340 | void Send(string message); 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /Book/Chapter8/Logging/V1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Book.Chapter8.Logging 5 | { 6 | public class User 7 | { 8 | private Logger _logger; 9 | public int UserId { get; set; } 10 | public string Email { get; private set; } 11 | public UserType Type { get; private set; } 12 | public bool IsEmailConfirmed { get; } 13 | public List EmailChangedEvents { get; } 14 | 15 | public User(int userId, string email, UserType type, bool isEmailConfirmed) 16 | { 17 | UserId = userId; 18 | Email = email; 19 | Type = type; 20 | IsEmailConfirmed = isEmailConfirmed; 21 | EmailChangedEvents = new List(); 22 | } 23 | 24 | public string CanChangeEmail() 25 | { 26 | if (IsEmailConfirmed) 27 | return "Can't change email after it's confirmed"; 28 | 29 | return null; 30 | } 31 | 32 | public void ChangeEmail(string newEmail, Company company) 33 | { 34 | Precondition.Requires(CanChangeEmail() == null); 35 | 36 | if (Email == newEmail) 37 | return; 38 | 39 | UserType newType = company.IsEmailCorporate(newEmail) 40 | ? UserType.Employee 41 | : UserType.Customer; 42 | 43 | if (Type != newType) 44 | { 45 | int delta = newType == UserType.Employee ? 1 : -1; 46 | company.ChangeNumberOfEmployees(delta); 47 | _logger.Info( 48 | $"User {UserId} changed type " + 49 | $"from {Type} to {newType}"); 50 | } 51 | 52 | Email = newEmail; 53 | Type = newType; 54 | EmailChangedEvents.Add(new EmailChangedEvent(UserId, newEmail)); 55 | } 56 | } 57 | 58 | internal class Logger 59 | { 60 | public void Info(string s) 61 | { 62 | } 63 | } 64 | 65 | public class EmailChangedEvent 66 | { 67 | public int UserId { get; } 68 | public string NewEmail { get; } 69 | 70 | public EmailChangedEvent(int userId, string newEmail) 71 | { 72 | UserId = userId; 73 | NewEmail = newEmail; 74 | } 75 | 76 | protected bool Equals(EmailChangedEvent other) 77 | { 78 | return UserId == other.UserId && string.Equals(NewEmail, other.NewEmail); 79 | } 80 | 81 | public override bool Equals(object obj) 82 | { 83 | if (ReferenceEquals(null, obj)) 84 | { 85 | return false; 86 | } 87 | 88 | if (ReferenceEquals(this, obj)) 89 | { 90 | return true; 91 | } 92 | 93 | if (obj.GetType() != this.GetType()) 94 | { 95 | return false; 96 | } 97 | 98 | return Equals((EmailChangedEvent)obj); 99 | } 100 | 101 | public override int GetHashCode() 102 | { 103 | unchecked 104 | { 105 | return (UserId * 397) ^ (NewEmail != null ? NewEmail.GetHashCode() : 0); 106 | } 107 | } 108 | } 109 | 110 | public class Company 111 | { 112 | public string DomainName { get; } 113 | public int NumberOfEmployees { get; private set; } 114 | 115 | public Company(string domainName, int numberOfEmployees) 116 | { 117 | DomainName = domainName; 118 | NumberOfEmployees = numberOfEmployees; 119 | } 120 | 121 | public void ChangeNumberOfEmployees(int delta) 122 | { 123 | Precondition.Requires(NumberOfEmployees + delta >= 0); 124 | 125 | NumberOfEmployees += delta; 126 | } 127 | 128 | public bool IsEmailCorporate(string email) 129 | { 130 | string emailDomain = email.Split('@')[1]; 131 | return emailDomain == DomainName; 132 | } 133 | } 134 | 135 | public enum UserType 136 | { 137 | Customer = 1, 138 | Employee = 2 139 | } 140 | 141 | public static class Precondition 142 | { 143 | public static void Requires(bool precondition, string message = null) 144 | { 145 | if (precondition == false) 146 | throw new Exception(message); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Book/Chapter8/LoggingV2/LoggingV2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Book.Chapter8.LoggingV2 5 | { 6 | public class User 7 | { 8 | private ILogger _logger; 9 | private IDomainLogger _domainLogger; 10 | public int UserId { get; set; } 11 | public string Email { get; private set; } 12 | public UserType Type { get; private set; } 13 | public bool IsEmailConfirmed { get; } 14 | public List DomainEvents { get; } 15 | 16 | public User(int userId, string email, UserType type, bool isEmailConfirmed) 17 | { 18 | UserId = userId; 19 | Email = email; 20 | Type = type; 21 | IsEmailConfirmed = isEmailConfirmed; 22 | DomainEvents = new List(); 23 | } 24 | 25 | public string CanChangeEmail() 26 | { 27 | if (IsEmailConfirmed) 28 | return "Can't change email after it's confirmed"; 29 | 30 | return null; 31 | } 32 | 33 | public void ChangeEmail(string newEmail, Company company) 34 | { 35 | _logger.Info( 36 | $"Changing email for user {UserId} to {newEmail}"); 37 | 38 | Precondition.Requires(CanChangeEmail() == null); 39 | 40 | if (Email == newEmail) 41 | return; 42 | 43 | UserType newType = company.IsEmailCorporate(newEmail) 44 | ? UserType.Employee 45 | : UserType.Customer; 46 | 47 | if (Type != newType) 48 | { 49 | int delta = newType == UserType.Employee ? 1 : -1; 50 | company.ChangeNumberOfEmployees(delta); 51 | AddDomainEvent( 52 | new UserTypeChangedEvent(UserId, Type, newType)); 53 | } 54 | 55 | Email = newEmail; 56 | Type = newType; 57 | AddDomainEvent(new EmailChangedEvent(UserId, newEmail)); 58 | 59 | _logger.Info($"Email is changed for user {UserId}"); 60 | } 61 | 62 | private void AddDomainEvent(EmailChangedEvent userTypeChangedEvent) 63 | { 64 | } 65 | 66 | private void AddDomainEvent(UserTypeChangedEvent userTypeChangedEvent) 67 | { 68 | 69 | } 70 | } 71 | 72 | public class UserController 73 | { 74 | private readonly Database _database = new Database(); 75 | private readonly MessageBus _messageBus = new MessageBus(); 76 | 77 | public string ChangeEmail(int userId, string newEmail) 78 | { 79 | object[] userData = _database.GetUserById(userId); 80 | User user = UserFactory.Create(userData); 81 | 82 | string error = user.CanChangeEmail(); 83 | if (error != null) 84 | return error; 85 | 86 | object[] companyData = _database.GetCompany(); 87 | Company company = CompanyFactory.Create(companyData); 88 | 89 | user.ChangeEmail(newEmail, company); 90 | 91 | _database.SaveCompany(company); 92 | _database.SaveUser(user); 93 | EventDispatcher.Dispatch(user.DomainEvents); 94 | 95 | return "OK"; 96 | } 97 | } 98 | 99 | public class EventDispatcher 100 | { 101 | public static void Dispatch(List userDomainEvents) 102 | { 103 | //foreach (EmailChangedEvent ev in user.DomainEvents) 104 | //{ 105 | // _messageBus.SendEmailChangedMessage(ev.UserId, ev.NewEmail); 106 | //} 107 | } 108 | } 109 | 110 | public class UserFactory 111 | { 112 | public static User Create(object[] data) 113 | { 114 | return null; 115 | } 116 | } 117 | 118 | public class CompanyFactory 119 | { 120 | public static Company Create(object[] data) 121 | { 122 | Precondition.Requires(data.Length >= 2); 123 | 124 | string domainName = (string)data[0]; 125 | int numberOfEmployees = (int)data[1]; 126 | 127 | return new Company(domainName, numberOfEmployees); 128 | } 129 | } 130 | 131 | internal interface IDomainLogger 132 | { 133 | void UserTypeHasChanged(int userId, UserType oldType, UserType newType); 134 | } 135 | 136 | public class DomainLogger : IDomainLogger 137 | { 138 | private readonly ILogger _logger; 139 | 140 | public DomainLogger(ILogger logger) 141 | { 142 | _logger = logger; 143 | } 144 | 145 | public void UserTypeHasChanged( 146 | int userId, UserType oldType, UserType newType) 147 | { 148 | _logger.Info( 149 | $"User {userId} changed type " + 150 | $"from {oldType} to {newType}"); 151 | } 152 | } 153 | 154 | public interface ILogger 155 | { 156 | void Info(string s); 157 | } 158 | 159 | public class UserTypeChangedEvent 160 | { 161 | public int UserId { get; } 162 | public UserType OldType { get; } 163 | public UserType NewType { get; } 164 | 165 | public UserTypeChangedEvent(int userId, UserType oldType, UserType newType) 166 | { 167 | UserId = userId; 168 | OldType = oldType; 169 | NewType = newType; 170 | } 171 | 172 | protected bool Equals(UserTypeChangedEvent other) 173 | { 174 | return UserId == other.UserId && string.Equals(OldType, other.OldType); 175 | } 176 | 177 | public override bool Equals(object obj) 178 | { 179 | if (ReferenceEquals(null, obj)) 180 | { 181 | return false; 182 | } 183 | 184 | if (ReferenceEquals(this, obj)) 185 | { 186 | return true; 187 | } 188 | 189 | if (obj.GetType() != this.GetType()) 190 | { 191 | return false; 192 | } 193 | 194 | return Equals((EmailChangedEvent)obj); 195 | } 196 | 197 | public override int GetHashCode() 198 | { 199 | unchecked 200 | { 201 | return (UserId * 397) ^ OldType.GetHashCode(); 202 | } 203 | } 204 | } 205 | 206 | public class EmailChangedEvent 207 | { 208 | public int UserId { get; } 209 | public string NewEmail { get; } 210 | 211 | public EmailChangedEvent(int userId, string newEmail) 212 | { 213 | UserId = userId; 214 | NewEmail = newEmail; 215 | } 216 | 217 | protected bool Equals(EmailChangedEvent other) 218 | { 219 | return UserId == other.UserId && string.Equals(NewEmail, other.NewEmail); 220 | } 221 | 222 | public override bool Equals(object obj) 223 | { 224 | if (ReferenceEquals(null, obj)) 225 | { 226 | return false; 227 | } 228 | 229 | if (ReferenceEquals(this, obj)) 230 | { 231 | return true; 232 | } 233 | 234 | if (obj.GetType() != this.GetType()) 235 | { 236 | return false; 237 | } 238 | 239 | return Equals((EmailChangedEvent)obj); 240 | } 241 | 242 | public override int GetHashCode() 243 | { 244 | unchecked 245 | { 246 | return (UserId * 397) ^ (NewEmail != null ? NewEmail.GetHashCode() : 0); 247 | } 248 | } 249 | } 250 | 251 | public class Company 252 | { 253 | public string DomainName { get; } 254 | public int NumberOfEmployees { get; private set; } 255 | 256 | public Company(string domainName, int numberOfEmployees) 257 | { 258 | DomainName = domainName; 259 | NumberOfEmployees = numberOfEmployees; 260 | } 261 | 262 | public void ChangeNumberOfEmployees(int delta) 263 | { 264 | Precondition.Requires(NumberOfEmployees + delta >= 0); 265 | 266 | NumberOfEmployees += delta; 267 | } 268 | 269 | public bool IsEmailCorporate(string email) 270 | { 271 | string emailDomain = email.Split('@')[1]; 272 | return emailDomain == DomainName; 273 | } 274 | } 275 | 276 | public enum UserType 277 | { 278 | Customer = 1, 279 | Employee = 2 280 | } 281 | 282 | public static class Precondition 283 | { 284 | public static void Requires(bool precondition, string message = null) 285 | { 286 | if (precondition == false) 287 | throw new Exception(message); 288 | } 289 | } 290 | 291 | public class Database 292 | { 293 | public object[] GetUserById(int userId) 294 | { 295 | return null; 296 | } 297 | 298 | public User GetUserByEmail(string email) 299 | { 300 | return null; 301 | } 302 | 303 | public void SaveUser(User user) 304 | { 305 | } 306 | 307 | public object[] GetCompany() 308 | { 309 | return null; 310 | } 311 | 312 | public void SaveCompany(Company company) 313 | { 314 | } 315 | } 316 | 317 | public class MessageBus 318 | { 319 | private IBus _bus; 320 | 321 | public void SendEmailChangedMessage(int userId, string newEmail) 322 | { 323 | _bus.Send($"Subject: USER; Type: EMAIL CHANGED; Id: {userId}; NewEmail: {newEmail}"); 324 | } 325 | } 326 | 327 | internal interface IBus 328 | { 329 | void Send(string message); 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /Book/Chapter8/NonCircular/NonCircular.cs: -------------------------------------------------------------------------------- 1 | namespace Book.Chapter8.NonCircular 2 | { 3 | public class CheckOutService 4 | { 5 | public void CheckOut(int orderId) 6 | { 7 | var service = new ReportGenerationService(); 8 | Report report = service.GenerateReport(orderId); 9 | 10 | /* other work */ 11 | } 12 | } 13 | 14 | public class ReportGenerationService 15 | { 16 | public Report GenerateReport(int orderId) 17 | { 18 | /* ... */ 19 | 20 | return null; 21 | } 22 | } 23 | 24 | public class Report 25 | { 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Book/Chapter8/Version1/V1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.SqlClient; 4 | using System.Dynamic; 5 | using System.Linq; 6 | using Dapper; 7 | using FluentAssertions; 8 | using Moq; 9 | using Xunit; 10 | 11 | namespace Book.Chapter8.Version1 12 | { 13 | public class User 14 | { 15 | public int UserId { get; set; } 16 | public string Email { get; private set; } 17 | public UserType Type { get; private set; } 18 | public bool IsEmailConfirmed { get; private set; } 19 | public List EmailChangedEvents { get; private set; } 20 | 21 | public User(int userId, string email, UserType type, bool isEmailConfirmed) 22 | { 23 | UserId = userId; 24 | Email = email; 25 | Type = type; 26 | IsEmailConfirmed = isEmailConfirmed; 27 | EmailChangedEvents = new List(); 28 | } 29 | 30 | public string CanChangeEmail() 31 | { 32 | if (IsEmailConfirmed) 33 | return "Can't change email after it's confirmed"; 34 | 35 | return null; 36 | } 37 | 38 | public void ChangeEmail(string newEmail, Company company) 39 | { 40 | Precondition.Requires(CanChangeEmail() == null); 41 | 42 | if (Email == newEmail) 43 | return; 44 | 45 | UserType newType = company.IsEmailCorporate(newEmail) 46 | ? UserType.Employee 47 | : UserType.Customer; 48 | 49 | if (Type != newType) 50 | { 51 | int delta = newType == UserType.Employee ? 1 : -1; 52 | company.ChangeNumberOfEmployees(delta); 53 | } 54 | 55 | Email = newEmail; 56 | Type = newType; 57 | EmailChangedEvents.Add(new EmailChangedEvent(UserId, newEmail)); 58 | } 59 | } 60 | 61 | public class UserController 62 | { 63 | private readonly Database _database; 64 | private readonly IMessageBus _messageBus; 65 | 66 | public UserController(Database database, IMessageBus messageBus) 67 | { 68 | _database = database; 69 | _messageBus = messageBus; 70 | } 71 | 72 | public string ChangeEmail(int userId, string newEmail) 73 | { 74 | object[] userData = _database.GetUserById(userId); 75 | User user = UserFactory.Create(userData); 76 | 77 | string error = user.CanChangeEmail(); 78 | if (error != null) 79 | return error; 80 | 81 | object[] companyData = _database.GetCompany(); 82 | Company company = CompanyFactory.Create(companyData); 83 | 84 | user.ChangeEmail(newEmail, company); 85 | 86 | _database.SaveCompany(company); 87 | _database.SaveUser(user); 88 | foreach (EmailChangedEvent ev in user.EmailChangedEvents) 89 | { 90 | _messageBus.SendEmailChangedMessage(ev.UserId, ev.NewEmail); 91 | } 92 | 93 | return "OK"; 94 | } 95 | } 96 | 97 | public class IntegrationTests 98 | { 99 | private const string ConnectionString = @"Server=.\Sql;Database=IntegrationTests;Trusted_Connection=true;"; 100 | 101 | [Fact] 102 | public void Changing_email_from_corporate_to_non_corporate() 103 | { 104 | // Arrange 105 | var db = new Database(ConnectionString); 106 | User user = CreateUser( 107 | "user@mycorp.com", UserType.Employee, db); 108 | CreateCompany("mycorp.com", 1, db); 109 | 110 | var messageBusMock = new Mock(); 111 | var sut = new UserController(db, messageBusMock.Object); 112 | 113 | // Act 114 | string result = sut.ChangeEmail(user.UserId, "new@gmail.com"); 115 | 116 | // Assert 117 | Assert.Equal("OK", result); 118 | 119 | object[] userData = db.GetUserById(user.UserId); 120 | User userFromDb = UserFactory.Create(userData); 121 | Assert.Equal("new@gmail.com", userFromDb.Email); 122 | Assert.Equal(UserType.Customer, userFromDb.Type); 123 | 124 | object[] companyData = db.GetCompany(); 125 | Company companyFromDb = CompanyFactory.Create(companyData); 126 | Assert.Equal(0, companyFromDb.NumberOfEmployees); 127 | 128 | messageBusMock.Verify( 129 | x => x.SendEmailChangedMessage(user.UserId, "new@gmail.com"), 130 | Times.Once); 131 | } 132 | 133 | private Company CreateCompany(string domainName, int numberOfEmployees, Database database) 134 | { 135 | var company = new Company(domainName, numberOfEmployees); 136 | database.SaveCompany(company); 137 | return company; 138 | } 139 | 140 | private User CreateUser(string email, UserType type, Database database) 141 | { 142 | var user = new User(0, email, type, false); 143 | database.SaveUser(user); 144 | return user; 145 | } 146 | } 147 | 148 | public class Database 149 | { 150 | private readonly string _connectionString; 151 | 152 | public Database(string connectionString) 153 | { 154 | _connectionString = connectionString; 155 | } 156 | 157 | public object[] GetUserById(int userId) 158 | { 159 | using (SqlConnection connection = new SqlConnection(_connectionString)) 160 | { 161 | string query = "SELECT * FROM [dbo].[User] WHERE UserID = @UserID"; 162 | dynamic data = connection.QuerySingle(query, new { UserID = userId }); 163 | 164 | return new object[] 165 | { 166 | data.UserID, 167 | data.Email, 168 | data.Type, 169 | data.IsEmailConfirmed 170 | }; 171 | } 172 | } 173 | 174 | public void SaveUser(User user) 175 | { 176 | using (SqlConnection connection = new SqlConnection(_connectionString)) 177 | { 178 | string updateQuery = @" 179 | UPDATE [dbo].[User] 180 | SET Email = @Email, Type = @Type, IsEmailConfirmed = @IsEmailConfirmed 181 | WHERE UserID = @UserID 182 | SELECT @UserID"; 183 | 184 | string insertQuery = @" 185 | INSERT [dbo].[User] (Email, Type, IsEmailConfirmed) 186 | VALUES (@Email, @Type, @IsEmailConfirmed) 187 | SELECT CAST(SCOPE_IDENTITY() as int)"; 188 | 189 | string query = user.UserId == 0 ? insertQuery : updateQuery; 190 | int userId = connection.Query(query, new 191 | { 192 | user.Email, 193 | user.UserId, 194 | user.IsEmailConfirmed, 195 | Type = (int)user.Type 196 | }) 197 | .Single(); 198 | 199 | user.UserId = userId; 200 | } 201 | } 202 | 203 | public object[] GetCompany() 204 | { 205 | using (SqlConnection connection = new SqlConnection(_connectionString)) 206 | { 207 | string query = "SELECT * FROM dbo.Company"; 208 | dynamic data = connection.QuerySingle(query); 209 | 210 | return new object[] 211 | { 212 | data.DomainName, 213 | data.NumberOfEmployees 214 | }; 215 | } 216 | } 217 | 218 | public void SaveCompany(Company company) 219 | { 220 | using (SqlConnection connection = new SqlConnection(_connectionString)) 221 | { 222 | string query = @" 223 | UPDATE dbo.Company 224 | SET DomainName = @DomainName, NumberOfEmployees = @NumberOfEmployees"; 225 | 226 | connection.Execute(query, new 227 | { 228 | company.DomainName, 229 | company.NumberOfEmployees 230 | }); 231 | } 232 | } 233 | } 234 | 235 | public class EmailChangedEvent 236 | { 237 | public int UserId { get; } 238 | public string NewEmail { get; } 239 | 240 | public EmailChangedEvent(int userId, string newEmail) 241 | { 242 | UserId = userId; 243 | NewEmail = newEmail; 244 | } 245 | 246 | protected bool Equals(EmailChangedEvent other) 247 | { 248 | return UserId == other.UserId && string.Equals(NewEmail, other.NewEmail); 249 | } 250 | 251 | public override bool Equals(object obj) 252 | { 253 | if (ReferenceEquals(null, obj)) 254 | { 255 | return false; 256 | } 257 | 258 | if (ReferenceEquals(this, obj)) 259 | { 260 | return true; 261 | } 262 | 263 | if (obj.GetType() != this.GetType()) 264 | { 265 | return false; 266 | } 267 | 268 | return Equals((EmailChangedEvent)obj); 269 | } 270 | 271 | public override int GetHashCode() 272 | { 273 | unchecked 274 | { 275 | return (UserId * 397) ^ (NewEmail != null ? NewEmail.GetHashCode() : 0); 276 | } 277 | } 278 | } 279 | 280 | public class UserFactory 281 | { 282 | public static User Create(object[] data) 283 | { 284 | Precondition.Requires(data.Length >= 3); 285 | 286 | int id = (int)data[0]; 287 | string email = (string)data[1]; 288 | UserType type = (UserType)data[2]; 289 | bool isEmailConfirmed = (bool)data[3]; 290 | 291 | return new User(id, email, type, isEmailConfirmed); 292 | } 293 | } 294 | 295 | public class Company 296 | { 297 | public string DomainName { get; private set; } 298 | public int NumberOfEmployees { get; private set; } 299 | 300 | public Company(string domainName, int numberOfEmployees) 301 | { 302 | DomainName = domainName; 303 | NumberOfEmployees = numberOfEmployees; 304 | } 305 | 306 | public void ChangeNumberOfEmployees(int delta) 307 | { 308 | Precondition.Requires(NumberOfEmployees + delta >= 0); 309 | 310 | NumberOfEmployees += delta; 311 | } 312 | 313 | public bool IsEmailCorporate(string email) 314 | { 315 | string emailDomain = email.Split('@')[1]; 316 | return emailDomain == DomainName; 317 | } 318 | } 319 | 320 | public class CompanyFactory 321 | { 322 | public static Company Create(object[] data) 323 | { 324 | Precondition.Requires(data.Length >= 2); 325 | 326 | string domainName = (string)data[0]; 327 | int numberOfEmployees = (int)data[1]; 328 | 329 | return new Company(domainName, numberOfEmployees); 330 | } 331 | } 332 | 333 | public enum UserType 334 | { 335 | Customer = 1, 336 | Employee = 2 337 | } 338 | 339 | public static class Precondition 340 | { 341 | public static void Requires(bool precondition, string message = null) 342 | { 343 | if (precondition == false) 344 | throw new Exception(message); 345 | } 346 | } 347 | 348 | public interface IMessageBus 349 | { 350 | void SendEmailChangedMessage(int userId, string newEmail); 351 | } 352 | 353 | public class MessageBus : IMessageBus 354 | { 355 | private IBus _bus; 356 | 357 | public void SendEmailChangedMessage(int userId, string newEmail) 358 | { 359 | _bus.Send($"Subject: USER; Type: EMAIL CHANGED; Id: {userId}; NewEmail: {newEmail}"); 360 | } 361 | } 362 | 363 | internal interface IBus 364 | { 365 | void Send(string message); 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /Book/Chapter9/V1/V1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.SqlClient; 4 | using System.Linq; 5 | using Dapper; 6 | using Moq; 7 | using Xunit; 8 | 9 | namespace Book.Chapter9.V1 10 | { 11 | public class User 12 | { 13 | public int UserId { get; set; } 14 | public string Email { get; private set; } 15 | public UserType Type { get; private set; } 16 | public bool IsEmailConfirmed { get; } 17 | public List DomainEvents { get; } 18 | 19 | public User(int userId, string email, UserType type, bool isEmailConfirmed) 20 | { 21 | UserId = userId; 22 | Email = email; 23 | Type = type; 24 | IsEmailConfirmed = isEmailConfirmed; 25 | DomainEvents = new List(); 26 | } 27 | 28 | public string CanChangeEmail() 29 | { 30 | if (IsEmailConfirmed) 31 | return "Can't change email after it's confirmed"; 32 | 33 | return null; 34 | } 35 | 36 | public void ChangeEmail(string newEmail, Company company) 37 | { 38 | Precondition.Requires(CanChangeEmail() == null); 39 | 40 | if (Email == newEmail) 41 | return; 42 | 43 | UserType newType = company.IsEmailCorporate(newEmail) 44 | ? UserType.Employee 45 | : UserType.Customer; 46 | 47 | if (Type != newType) 48 | { 49 | int delta = newType == UserType.Employee ? 1 : -1; 50 | company.ChangeNumberOfEmployees(delta); 51 | AddDomainEvent(new UserTypeChangedEvent(UserId, Type, newType)); 52 | } 53 | 54 | Email = newEmail; 55 | Type = newType; 56 | AddDomainEvent(new EmailChangedEvent(UserId, newEmail)); 57 | } 58 | 59 | private void AddDomainEvent(IDomainEvent domainEvent) 60 | { 61 | DomainEvents.Add(domainEvent); 62 | } 63 | } 64 | 65 | public class UserController 66 | { 67 | private readonly Database _database; 68 | private readonly EventDispatcher _eventDispatcher; 69 | 70 | public UserController( 71 | Database database, 72 | IMessageBus messageBus, 73 | IDomainLogger domainLogger) 74 | { 75 | _database = database; 76 | _eventDispatcher = new EventDispatcher( 77 | messageBus, domainLogger); 78 | } 79 | 80 | public string ChangeEmail(int userId, string newEmail) 81 | { 82 | object[] userData = _database.GetUserById(userId); 83 | User user = UserFactory.Create(userData); 84 | 85 | string error = user.CanChangeEmail(); 86 | if (error != null) 87 | return error; 88 | 89 | object[] companyData = _database.GetCompany(); 90 | Company company = CompanyFactory.Create(companyData); 91 | 92 | user.ChangeEmail(newEmail, company); 93 | 94 | _database.SaveCompany(company); 95 | _database.SaveUser(user); 96 | _eventDispatcher.Dispatch(user.DomainEvents); 97 | 98 | return "OK"; 99 | } 100 | } 101 | 102 | public class EventDispatcher 103 | { 104 | private readonly IMessageBus _messageBus; 105 | private readonly IDomainLogger _domainLogger; 106 | 107 | public EventDispatcher( 108 | IMessageBus messageBus, 109 | IDomainLogger domainLogger) 110 | { 111 | _domainLogger = domainLogger; 112 | _messageBus = messageBus; 113 | } 114 | 115 | public void Dispatch(List events) 116 | { 117 | foreach (IDomainEvent ev in events) 118 | { 119 | Dispatch(ev); 120 | } 121 | } 122 | 123 | private void Dispatch(IDomainEvent ev) 124 | { 125 | switch (ev) 126 | { 127 | case EmailChangedEvent emailChangedEvent: 128 | _messageBus.SendEmailChangedMessage( 129 | emailChangedEvent.UserId, 130 | emailChangedEvent.NewEmail); 131 | break; 132 | 133 | case UserTypeChangedEvent userTypeChangedEvent: 134 | _domainLogger.UserTypeHasChanged( 135 | userTypeChangedEvent.UserId, 136 | userTypeChangedEvent.OldType, 137 | userTypeChangedEvent.NewType); 138 | break; 139 | } 140 | } 141 | } 142 | 143 | public class UserFactory 144 | { 145 | public static User Create(object[] data) 146 | { 147 | Precondition.Requires(data.Length >= 3); 148 | 149 | int id = (int)data[0]; 150 | string email = (string)data[1]; 151 | UserType type = (UserType)data[2]; 152 | bool isEmailConfirmed = (bool)data[3]; 153 | 154 | return new User(id, email, type, isEmailConfirmed); 155 | } 156 | } 157 | 158 | public class CompanyFactory 159 | { 160 | public static Company Create(object[] data) 161 | { 162 | Precondition.Requires(data.Length >= 2); 163 | 164 | string domainName = (string)data[0]; 165 | int numberOfEmployees = (int)data[1]; 166 | 167 | return new Company(domainName, numberOfEmployees); 168 | } 169 | } 170 | 171 | public interface IDomainLogger 172 | { 173 | void UserTypeHasChanged(int userId, UserType oldType, UserType newType); 174 | } 175 | 176 | public class DomainLogger : IDomainLogger 177 | { 178 | private readonly ILogger _logger; 179 | 180 | public DomainLogger(ILogger logger) 181 | { 182 | _logger = logger; 183 | } 184 | 185 | public void UserTypeHasChanged( 186 | int userId, UserType oldType, UserType newType) 187 | { 188 | _logger.Info( 189 | $"User {userId} changed type " + 190 | $"from {oldType} to {newType}"); 191 | } 192 | } 193 | 194 | public interface ILogger 195 | { 196 | void Info(string s); 197 | } 198 | 199 | public class UserTypeChangedEvent : IDomainEvent 200 | { 201 | public int UserId { get; } 202 | public UserType OldType { get; } 203 | public UserType NewType { get; } 204 | 205 | public UserTypeChangedEvent(int userId, UserType oldType, UserType newType) 206 | { 207 | UserId = userId; 208 | OldType = oldType; 209 | NewType = newType; 210 | } 211 | 212 | protected bool Equals(UserTypeChangedEvent other) 213 | { 214 | return UserId == other.UserId && string.Equals(OldType, other.OldType); 215 | } 216 | 217 | public override bool Equals(object obj) 218 | { 219 | if (ReferenceEquals(null, obj)) 220 | { 221 | return false; 222 | } 223 | 224 | if (ReferenceEquals(this, obj)) 225 | { 226 | return true; 227 | } 228 | 229 | if (obj.GetType() != this.GetType()) 230 | { 231 | return false; 232 | } 233 | 234 | return Equals((EmailChangedEvent)obj); 235 | } 236 | 237 | public override int GetHashCode() 238 | { 239 | unchecked 240 | { 241 | return (UserId * 397) ^ OldType.GetHashCode(); 242 | } 243 | } 244 | } 245 | 246 | public class EmailChangedEvent : IDomainEvent 247 | { 248 | public int UserId { get; } 249 | public string NewEmail { get; } 250 | 251 | public EmailChangedEvent(int userId, string newEmail) 252 | { 253 | UserId = userId; 254 | NewEmail = newEmail; 255 | } 256 | 257 | protected bool Equals(EmailChangedEvent other) 258 | { 259 | return UserId == other.UserId && string.Equals(NewEmail, other.NewEmail); 260 | } 261 | 262 | public override bool Equals(object obj) 263 | { 264 | if (ReferenceEquals(null, obj)) 265 | { 266 | return false; 267 | } 268 | 269 | if (ReferenceEquals(this, obj)) 270 | { 271 | return true; 272 | } 273 | 274 | if (obj.GetType() != this.GetType()) 275 | { 276 | return false; 277 | } 278 | 279 | return Equals((EmailChangedEvent)obj); 280 | } 281 | 282 | public override int GetHashCode() 283 | { 284 | unchecked 285 | { 286 | return (UserId * 397) ^ (NewEmail != null ? NewEmail.GetHashCode() : 0); 287 | } 288 | } 289 | } 290 | 291 | public interface IDomainEvent 292 | { 293 | } 294 | 295 | public class Company 296 | { 297 | public string DomainName { get; } 298 | public int NumberOfEmployees { get; private set; } 299 | 300 | public Company(string domainName, int numberOfEmployees) 301 | { 302 | DomainName = domainName; 303 | NumberOfEmployees = numberOfEmployees; 304 | } 305 | 306 | public void ChangeNumberOfEmployees(int delta) 307 | { 308 | Precondition.Requires(NumberOfEmployees + delta >= 0); 309 | 310 | NumberOfEmployees += delta; 311 | } 312 | 313 | public bool IsEmailCorporate(string email) 314 | { 315 | string emailDomain = email.Split('@')[1]; 316 | return emailDomain == DomainName; 317 | } 318 | } 319 | 320 | public enum UserType 321 | { 322 | Customer = 1, 323 | Employee = 2 324 | } 325 | 326 | public static class Precondition 327 | { 328 | public static void Requires(bool precondition, string message = null) 329 | { 330 | if (precondition == false) 331 | throw new Exception(message); 332 | } 333 | } 334 | 335 | public class IntegrationTests 336 | { 337 | private const string ConnectionString = @"Server=.\Sql;Database=IntegrationTests;Trusted_Connection=true;"; 338 | 339 | [Fact] 340 | public void Changing_email_from_corporate_to_non_corporate() 341 | { 342 | // Arrange 343 | var db = new Database(ConnectionString); 344 | User user = CreateUser("user@mycorp.com", UserType.Employee, db); 345 | CreateCompany("mycorp.com", 1, db); 346 | 347 | var messageBusMock = new Mock(); 348 | var loggerMock = new Mock(); 349 | var sut = new UserController( 350 | db, messageBusMock.Object, loggerMock.Object); 351 | 352 | // Act 353 | string result = sut.ChangeEmail(user.UserId, "new@gmail.com"); 354 | 355 | // Assert 356 | Assert.Equal("OK", result); 357 | 358 | object[] userData = db.GetUserById(user.UserId); 359 | User userFromDb = UserFactory.Create(userData); 360 | Assert.Equal("new@gmail.com", userFromDb.Email); 361 | Assert.Equal(UserType.Customer, userFromDb.Type); 362 | 363 | object[] companyData = db.GetCompany(); 364 | Company companyFromDb = CompanyFactory.Create(companyData); 365 | Assert.Equal(0, companyFromDb.NumberOfEmployees); 366 | 367 | messageBusMock.Verify( 368 | x => x.SendEmailChangedMessage(user.UserId, "new@gmail.com"), 369 | Times.Once); 370 | loggerMock.Verify( 371 | x => x.UserTypeHasChanged( 372 | user.UserId, UserType.Employee, UserType.Customer), 373 | Times.Once); 374 | } 375 | 376 | private Company CreateCompany(string domainName, int numberOfEmployees, Database database) 377 | { 378 | var company = new Company(domainName, numberOfEmployees); 379 | database.SaveCompany(company); 380 | return company; 381 | } 382 | 383 | private User CreateUser(string email, UserType type, Database database) 384 | { 385 | var user = new User(0, email, type, false); 386 | database.SaveUser(user); 387 | return user; 388 | } 389 | } 390 | 391 | public class Database 392 | { 393 | private readonly string _connectionString; 394 | 395 | public Database(string connectionString) 396 | { 397 | _connectionString = connectionString; 398 | } 399 | 400 | public object[] GetUserById(int userId) 401 | { 402 | using (SqlConnection connection = new SqlConnection(_connectionString)) 403 | { 404 | string query = "SELECT * FROM [dbo].[User] WHERE UserID = @UserID"; 405 | dynamic data = connection.QuerySingle(query, new { UserID = userId }); 406 | 407 | return new object[] 408 | { 409 | data.UserID, 410 | data.Email, 411 | data.Type, 412 | data.IsEmailConfirmed 413 | }; 414 | } 415 | } 416 | 417 | public void SaveUser(User user) 418 | { 419 | using (SqlConnection connection = new SqlConnection(_connectionString)) 420 | { 421 | string updateQuery = @" 422 | UPDATE [dbo].[User] 423 | SET Email = @Email, Type = @Type, IsEmailConfirmed = @IsEmailConfirmed 424 | WHERE UserID = @UserID 425 | SELECT @UserID"; 426 | 427 | string insertQuery = @" 428 | INSERT [dbo].[User] (Email, Type, IsEmailConfirmed) 429 | VALUES (@Email, @Type, @IsEmailConfirmed) 430 | SELECT CAST(SCOPE_IDENTITY() as int)"; 431 | 432 | string query = user.UserId == 0 ? insertQuery : updateQuery; 433 | int userId = connection.Query(query, new 434 | { 435 | user.Email, 436 | user.UserId, 437 | user.IsEmailConfirmed, 438 | Type = (int)user.Type 439 | }) 440 | .Single(); 441 | 442 | user.UserId = userId; 443 | } 444 | } 445 | 446 | public object[] GetCompany() 447 | { 448 | using (SqlConnection connection = new SqlConnection(_connectionString)) 449 | { 450 | string query = "SELECT * FROM dbo.Company"; 451 | dynamic data = connection.QuerySingle(query); 452 | 453 | return new object[] 454 | { 455 | data.DomainName, 456 | data.NumberOfEmployees 457 | }; 458 | } 459 | } 460 | 461 | public void SaveCompany(Company company) 462 | { 463 | using (SqlConnection connection = new SqlConnection(_connectionString)) 464 | { 465 | string query = @" 466 | UPDATE dbo.Company 467 | SET DomainName = @DomainName, NumberOfEmployees = @NumberOfEmployees"; 468 | 469 | connection.Execute(query, new 470 | { 471 | company.DomainName, 472 | company.NumberOfEmployees 473 | }); 474 | } 475 | } 476 | } 477 | 478 | public interface IMessageBus 479 | { 480 | void SendEmailChangedMessage(int userId, string newEmail); 481 | } 482 | 483 | public class MessageBus : IMessageBus 484 | { 485 | private readonly IBus _bus; 486 | 487 | public void SendEmailChangedMessage(int userId, string newEmail) 488 | { 489 | _bus.Send("Type: USER EMAIL CHANGED; " + 490 | $"Id: {userId}; " + 491 | $"NewEmail: {newEmail}"); 492 | } 493 | } 494 | 495 | public interface IBus 496 | { 497 | void Send(string message); 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /Book/Chapter9/V2/V2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.SqlClient; 4 | using System.Linq; 5 | using Dapper; 6 | using Moq; 7 | using Xunit; 8 | 9 | namespace Book.Chapter9.V2 10 | { 11 | public class User 12 | { 13 | public int UserId { get; set; } 14 | public string Email { get; private set; } 15 | public UserType Type { get; private set; } 16 | public bool IsEmailConfirmed { get; } 17 | public List DomainEvents { get; } 18 | 19 | public User(int userId, string email, UserType type, bool isEmailConfirmed) 20 | { 21 | UserId = userId; 22 | Email = email; 23 | Type = type; 24 | IsEmailConfirmed = isEmailConfirmed; 25 | DomainEvents = new List(); 26 | } 27 | 28 | public string CanChangeEmail() 29 | { 30 | if (IsEmailConfirmed) 31 | return "Can't change email after it's confirmed"; 32 | 33 | return null; 34 | } 35 | 36 | public void ChangeEmail(string newEmail, Company company) 37 | { 38 | Precondition.Requires(CanChangeEmail() == null); 39 | 40 | if (Email == newEmail) 41 | return; 42 | 43 | UserType newType = company.IsEmailCorporate(newEmail) 44 | ? UserType.Employee 45 | : UserType.Customer; 46 | 47 | if (Type != newType) 48 | { 49 | int delta = newType == UserType.Employee ? 1 : -1; 50 | company.ChangeNumberOfEmployees(delta); 51 | AddDomainEvent(new UserTypeChangedEvent(UserId, Type, newType)); 52 | } 53 | 54 | Email = newEmail; 55 | Type = newType; 56 | AddDomainEvent(new EmailChangedEvent(UserId, newEmail)); 57 | } 58 | 59 | private void AddDomainEvent(IDomainEvent domainEvent) 60 | { 61 | DomainEvents.Add(domainEvent); 62 | } 63 | } 64 | 65 | public class UserController 66 | { 67 | private readonly Database _database; 68 | private readonly EventDispatcher _eventDispatcher; 69 | 70 | public UserController( 71 | Database database, 72 | MessageBus messageBus, 73 | IDomainLogger domainLogger) 74 | { 75 | _database = database; 76 | _eventDispatcher = new EventDispatcher( 77 | messageBus, domainLogger); 78 | } 79 | 80 | public string ChangeEmail(int userId, string newEmail) 81 | { 82 | object[] userData = _database.GetUserById(userId); 83 | User user = UserFactory.Create(userData); 84 | 85 | string error = user.CanChangeEmail(); 86 | if (error != null) 87 | return error; 88 | 89 | object[] companyData = _database.GetCompany(); 90 | Company company = CompanyFactory.Create(companyData); 91 | 92 | user.ChangeEmail(newEmail, company); 93 | 94 | _database.SaveCompany(company); 95 | _database.SaveUser(user); 96 | _eventDispatcher.Dispatch(user.DomainEvents); 97 | 98 | return "OK"; 99 | } 100 | } 101 | 102 | public class EventDispatcher 103 | { 104 | private readonly MessageBus _messageBus; 105 | private readonly IDomainLogger _domainLogger; 106 | 107 | public EventDispatcher( 108 | MessageBus messageBus, 109 | IDomainLogger domainLogger) 110 | { 111 | _domainLogger = domainLogger; 112 | _messageBus = messageBus; 113 | } 114 | 115 | public void Dispatch(List events) 116 | { 117 | foreach (IDomainEvent ev in events) 118 | { 119 | Dispatch(ev); 120 | } 121 | } 122 | 123 | private void Dispatch(IDomainEvent ev) 124 | { 125 | switch (ev) 126 | { 127 | case EmailChangedEvent emailChangedEvent: 128 | _messageBus.SendEmailChangedMessage( 129 | emailChangedEvent.UserId, 130 | emailChangedEvent.NewEmail); 131 | break; 132 | 133 | case UserTypeChangedEvent userTypeChangedEvent: 134 | _domainLogger.UserTypeHasChanged( 135 | userTypeChangedEvent.UserId, 136 | userTypeChangedEvent.OldType, 137 | userTypeChangedEvent.NewType); 138 | break; 139 | } 140 | } 141 | } 142 | 143 | public class UserFactory 144 | { 145 | public static User Create(object[] data) 146 | { 147 | Precondition.Requires(data.Length >= 3); 148 | 149 | int id = (int)data[0]; 150 | string email = (string)data[1]; 151 | UserType type = (UserType)data[2]; 152 | bool isEmailConfirmed = (bool)data[3]; 153 | 154 | return new User(id, email, type, isEmailConfirmed); 155 | } 156 | } 157 | 158 | public class CompanyFactory 159 | { 160 | public static Company Create(object[] data) 161 | { 162 | Precondition.Requires(data.Length >= 2); 163 | 164 | string domainName = (string)data[0]; 165 | int numberOfEmployees = (int)data[1]; 166 | 167 | return new Company(domainName, numberOfEmployees); 168 | } 169 | } 170 | 171 | public interface IDomainLogger 172 | { 173 | void UserTypeHasChanged(int userId, UserType oldType, UserType newType); 174 | } 175 | 176 | public class DomainLogger : IDomainLogger 177 | { 178 | private readonly ILogger _logger; 179 | 180 | public DomainLogger(ILogger logger) 181 | { 182 | _logger = logger; 183 | } 184 | 185 | public void UserTypeHasChanged( 186 | int userId, UserType oldType, UserType newType) 187 | { 188 | _logger.Info( 189 | $"User {userId} changed type " + 190 | $"from {oldType} to {newType}"); 191 | } 192 | } 193 | 194 | public interface ILogger 195 | { 196 | void Info(string s); 197 | } 198 | 199 | public class UserTypeChangedEvent : IDomainEvent 200 | { 201 | public int UserId { get; } 202 | public UserType OldType { get; } 203 | public UserType NewType { get; } 204 | 205 | public UserTypeChangedEvent(int userId, UserType oldType, UserType newType) 206 | { 207 | UserId = userId; 208 | OldType = oldType; 209 | NewType = newType; 210 | } 211 | 212 | protected bool Equals(UserTypeChangedEvent other) 213 | { 214 | return UserId == other.UserId && string.Equals(OldType, other.OldType); 215 | } 216 | 217 | public override bool Equals(object obj) 218 | { 219 | if (ReferenceEquals(null, obj)) 220 | { 221 | return false; 222 | } 223 | 224 | if (ReferenceEquals(this, obj)) 225 | { 226 | return true; 227 | } 228 | 229 | if (obj.GetType() != this.GetType()) 230 | { 231 | return false; 232 | } 233 | 234 | return Equals((EmailChangedEvent)obj); 235 | } 236 | 237 | public override int GetHashCode() 238 | { 239 | unchecked 240 | { 241 | return (UserId * 397) ^ OldType.GetHashCode(); 242 | } 243 | } 244 | } 245 | 246 | public class EmailChangedEvent : IDomainEvent 247 | { 248 | public int UserId { get; } 249 | public string NewEmail { get; } 250 | 251 | public EmailChangedEvent(int userId, string newEmail) 252 | { 253 | UserId = userId; 254 | NewEmail = newEmail; 255 | } 256 | 257 | protected bool Equals(EmailChangedEvent other) 258 | { 259 | return UserId == other.UserId && string.Equals(NewEmail, other.NewEmail); 260 | } 261 | 262 | public override bool Equals(object obj) 263 | { 264 | if (ReferenceEquals(null, obj)) 265 | { 266 | return false; 267 | } 268 | 269 | if (ReferenceEquals(this, obj)) 270 | { 271 | return true; 272 | } 273 | 274 | if (obj.GetType() != this.GetType()) 275 | { 276 | return false; 277 | } 278 | 279 | return Equals((EmailChangedEvent)obj); 280 | } 281 | 282 | public override int GetHashCode() 283 | { 284 | unchecked 285 | { 286 | return (UserId * 397) ^ (NewEmail != null ? NewEmail.GetHashCode() : 0); 287 | } 288 | } 289 | } 290 | 291 | public interface IDomainEvent 292 | { 293 | } 294 | 295 | public class Company 296 | { 297 | public string DomainName { get; } 298 | public int NumberOfEmployees { get; private set; } 299 | 300 | public Company(string domainName, int numberOfEmployees) 301 | { 302 | DomainName = domainName; 303 | NumberOfEmployees = numberOfEmployees; 304 | } 305 | 306 | public void ChangeNumberOfEmployees(int delta) 307 | { 308 | Precondition.Requires(NumberOfEmployees + delta >= 0); 309 | 310 | NumberOfEmployees += delta; 311 | } 312 | 313 | public bool IsEmailCorporate(string email) 314 | { 315 | string emailDomain = email.Split('@')[1]; 316 | return emailDomain == DomainName; 317 | } 318 | } 319 | 320 | public enum UserType 321 | { 322 | Customer = 1, 323 | Employee = 2 324 | } 325 | 326 | public static class Precondition 327 | { 328 | public static void Requires(bool precondition, string message = null) 329 | { 330 | if (precondition == false) 331 | throw new Exception(message); 332 | } 333 | } 334 | 335 | public class IntegrationTests 336 | { 337 | private const string ConnectionString = @"Server=.\Sql;Database=IntegrationTests;Trusted_Connection=true;"; 338 | 339 | [Fact] 340 | public void Changing_email_from_corporate_to_non_corporate() 341 | { 342 | // Arrange 343 | var db = new Database(ConnectionString); 344 | User user = CreateUser("user@mycorp.com", UserType.Employee, db); 345 | CreateCompany("mycorp.com", 1, db); 346 | 347 | var busMock = new Mock(); 348 | var messageBus = new MessageBus(busMock.Object); 349 | var loggerMock = new Mock(); 350 | var sut = new UserController(db, messageBus, loggerMock.Object); 351 | 352 | // Act 353 | string result = sut.ChangeEmail(user.UserId, "new@gmail.com"); 354 | 355 | // Assert 356 | Assert.Equal("OK", result); 357 | 358 | object[] userData = db.GetUserById(user.UserId); 359 | User userFromDb = UserFactory.Create(userData); 360 | Assert.Equal("new@gmail.com", userFromDb.Email); 361 | Assert.Equal(UserType.Customer, userFromDb.Type); 362 | 363 | object[] companyData = db.GetCompany(); 364 | Company companyFromDb = CompanyFactory.Create(companyData); 365 | Assert.Equal(0, companyFromDb.NumberOfEmployees); 366 | 367 | busMock.Verify( 368 | x => x.Send( 369 | "Type: USER EMAIL CHANGED; " + 370 | $"Id: {user.UserId}; " + 371 | "NewEmail: new@gmail.com"), 372 | Times.Once); 373 | loggerMock.Verify( 374 | x => x.UserTypeHasChanged( 375 | user.UserId, UserType.Employee, UserType.Customer), 376 | Times.Once); 377 | } 378 | 379 | [Fact] 380 | public void Changing_email_from_corporate_to_non_corporate_spy() 381 | { 382 | // Arrange 383 | var db = new Database(ConnectionString); 384 | User user = CreateUser("user@mycorp.com", UserType.Employee, db); 385 | CreateCompany("mycorp.com", 1, db); 386 | 387 | var busSpy = new BusSpy(); 388 | var messageBus = new MessageBus(busSpy); 389 | var loggerMock = new Mock(); 390 | var sut = new UserController(db, messageBus, loggerMock.Object); 391 | 392 | // Act 393 | string result = sut.ChangeEmail(user.UserId, "new@gmail.com"); 394 | 395 | // Assert 396 | Assert.Equal("OK", result); 397 | 398 | object[] userData = db.GetUserById(user.UserId); 399 | User userFromDb = UserFactory.Create(userData); 400 | Assert.Equal("new@gmail.com", userFromDb.Email); 401 | Assert.Equal(UserType.Customer, userFromDb.Type); 402 | 403 | object[] companyData = db.GetCompany(); 404 | Company companyFromDb = CompanyFactory.Create(companyData); 405 | Assert.Equal(0, companyFromDb.NumberOfEmployees); 406 | 407 | busSpy.ShouldSendNumberOfMessages(1) 408 | .WithEmailChangedMessage(user.UserId, "new@gmail.com"); 409 | loggerMock.Verify( 410 | x => x.UserTypeHasChanged( 411 | user.UserId, UserType.Employee, UserType.Customer), 412 | Times.Once); 413 | } 414 | 415 | private Company CreateCompany(string domainName, int numberOfEmployees, Database database) 416 | { 417 | var company = new Company(domainName, numberOfEmployees); 418 | database.SaveCompany(company); 419 | return company; 420 | } 421 | 422 | private User CreateUser(string email, UserType type, Database database) 423 | { 424 | var user = new User(0, email, type, false); 425 | database.SaveUser(user); 426 | return user; 427 | } 428 | } 429 | 430 | public class Database 431 | { 432 | private readonly string _connectionString; 433 | 434 | public Database(string connectionString) 435 | { 436 | _connectionString = connectionString; 437 | } 438 | 439 | public object[] GetUserById(int userId) 440 | { 441 | using (SqlConnection connection = new SqlConnection(_connectionString)) 442 | { 443 | string query = "SELECT * FROM [dbo].[User] WHERE UserID = @UserID"; 444 | dynamic data = connection.QuerySingle(query, new { UserID = userId }); 445 | 446 | return new object[] 447 | { 448 | data.UserID, 449 | data.Email, 450 | data.Type, 451 | data.IsEmailConfirmed 452 | }; 453 | } 454 | } 455 | 456 | public void SaveUser(User user) 457 | { 458 | using (SqlConnection connection = new SqlConnection(_connectionString)) 459 | { 460 | string updateQuery = @" 461 | UPDATE [dbo].[User] 462 | SET Email = @Email, Type = @Type, IsEmailConfirmed = @IsEmailConfirmed 463 | WHERE UserID = @UserID 464 | SELECT @UserID"; 465 | 466 | string insertQuery = @" 467 | INSERT [dbo].[User] (Email, Type, IsEmailConfirmed) 468 | VALUES (@Email, @Type, @IsEmailConfirmed) 469 | SELECT CAST(SCOPE_IDENTITY() as int)"; 470 | 471 | string query = user.UserId == 0 ? insertQuery : updateQuery; 472 | int userId = connection.Query(query, new 473 | { 474 | user.Email, 475 | user.UserId, 476 | user.IsEmailConfirmed, 477 | Type = (int)user.Type 478 | }) 479 | .Single(); 480 | 481 | user.UserId = userId; 482 | } 483 | } 484 | 485 | public object[] GetCompany() 486 | { 487 | using (SqlConnection connection = new SqlConnection(_connectionString)) 488 | { 489 | string query = "SELECT * FROM dbo.Company"; 490 | dynamic data = connection.QuerySingle(query); 491 | 492 | return new object[] 493 | { 494 | data.DomainName, 495 | data.NumberOfEmployees 496 | }; 497 | } 498 | } 499 | 500 | public void SaveCompany(Company company) 501 | { 502 | using (SqlConnection connection = new SqlConnection(_connectionString)) 503 | { 504 | string query = @" 505 | UPDATE dbo.Company 506 | SET DomainName = @DomainName, NumberOfEmployees = @NumberOfEmployees"; 507 | 508 | connection.Execute(query, new 509 | { 510 | company.DomainName, 511 | company.NumberOfEmployees 512 | }); 513 | } 514 | } 515 | } 516 | 517 | public class MessageBus 518 | { 519 | private readonly IBus _bus; 520 | 521 | public MessageBus(IBus bus) 522 | { 523 | _bus = bus; 524 | } 525 | 526 | public void SendEmailChangedMessage(int userId, string newEmail) 527 | { 528 | _bus.Send("Type: USER EMAIL CHANGED; " + 529 | $"Id: {userId}; " + 530 | $"NewEmail: {newEmail}"); 531 | } 532 | } 533 | 534 | public interface IBus 535 | { 536 | void Send(string message); 537 | } 538 | 539 | public class BusSpy : IBus 540 | { 541 | private List _sentMessages = new List(); 542 | 543 | public void Send(string message) 544 | { 545 | _sentMessages.Add(message); 546 | } 547 | 548 | public BusSpy ShouldSendNumberOfMessages(int number) 549 | { 550 | Assert.Equal(number, _sentMessages.Count); 551 | return this; 552 | } 553 | 554 | public BusSpy WithEmailChangedMessage(int userId, string newEmail) 555 | { 556 | string message = "Type: USER EMAIL CHANGED; " + 557 | $"Id: {userId}; " + 558 | $"NewEmail: {newEmail}"; 559 | Assert.Contains(_sentMessages, x => x == message); 560 | 561 | return this; 562 | } 563 | } 564 | } 565 | -------------------------------------------------------------------------------- /DatabaseGenerationScript.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AcornPublishing/unit-testing/809615535f169ecf68eda6c51ee305fbea818ac3/DatabaseGenerationScript.sql -------------------------------------------------------------------------------- /ReadMe.txt: -------------------------------------------------------------------------------- 1 | To run integration tests: 2 | 3 | 1. Use the script in DatabaseGenerationScript.sql to create the database. 4 | 2. Configure the tests to execute sequentially, not in parallel. --------------------------------------------------------------------------------