├── .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