Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions domain-driven design/ValueObjects/Tests/MoneyValidationUnitTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using ValueObjects.ValueObjects;

namespace Tests;

public class MoneyValidationUnitTest
{
[Fact]
public void GivenANegativeAmount_WhenCreatingMoney_ThenItFails()
{
var result = Money.Create(-5, "USD");

Assert.False(result.IsSuccess);
Assert.Equal("Money.NegativeAmount", result.Error!.Code);
}

[Fact]
public void GivenAnUnsupportedCurrency_WhenCreatingMoney_ThenItFails()
{
var result = Money.Create(100, "GBP");

Assert.False(result.IsSuccess);
Assert.Equal("Money.UnsupportedCurrency", result.Error!.Code);
}

[Fact]
public void GivenALowerCaseCurrency_WhenCreatingMoney_ThenItEqualsTheUpperCaseOne()
{
var typed = Money.Create(100, "usd").Value;

Assert.Equal("USD", typed.Currency);
Assert.Equal(Money.Create(100, "USD").Value, typed);
}
}
35 changes: 35 additions & 0 deletions domain-driven design/ValueObjects/Tests/PersistenceUnitTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using ValueObjects.Entities;
using ValueObjects.Persistence;
using ValueObjects.ValueObjects;

namespace Tests;

public class PersistenceUnitTest
{
[Fact]
public async Task GivenASavedPayment_WhenQueryingByMoney_ThenItIsFound()
{
var cancellationToken = TestContext.Current.CancellationToken;
await using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync(cancellationToken);
var options = new DbContextOptionsBuilder<PaymentsDbContext>().UseSqlite(connection).Options;

await using (var context = new PaymentsDbContext(options))
{
await context.Database.EnsureCreatedAsync(cancellationToken);
context.Payments.Add(new Payment(Money.Create(100, "usd").Value));
await context.SaveChangesAsync(cancellationToken);
}

await using (var context = new PaymentsDbContext(options))
{
var price = Money.Create(100, "USD").Value;

var payment = await context.Payments.SingleAsync(p => p.Quoted == price, cancellationToken);

Assert.Equal(price, payment.Quoted);
}
}
}
16 changes: 16 additions & 0 deletions domain-driven design/ValueObjects/Tests/RouteEqualityUnitTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using ValueObjects.ValueObjects;

namespace Tests;

public class RouteEqualityUnitTest
{
[Fact]
public void GivenTwoRoutes_WhenTheyHaveTheSameStops_ThenTheyShouldBeEqual()
{
Station[] stops = [new("JFK", "John F. Kennedy International Airport"), new("YVR", "Vancouver International Airport")];
Station[] sameStops = [new("JFK", "John F. Kennedy International Airport"), new("YVR", "Vancouver International Airport")];

Assert.Equal(new Route(stops), new Route(sameStops));
Assert.Single(new HashSet<Route> { new(stops), new(sameStops) });
}
}
22 changes: 11 additions & 11 deletions domain-driven design/ValueObjects/Tests/SerializationUnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,26 @@ namespace Tests;

public sealed class SerializationUnitTest
{
[Test]
[Fact]
public void GivenAUserWithEmailAddressObject_WhenUsingEmailAddressConverter_ThenCanSerializeItIntoString()
{
var user = new User(new("email@example.com"));
var settings = new JsonSerializerOptions {Converters = {new EmailAddressConverter()}};
var settings = new JsonSerializerOptions { Converters = { new EmailAddressConverter() } };
var json = JsonSerializer.Serialize(user, settings);

const string expectedJson = "{\"EmailAddress\":\"email@example.com\"}";
Assert.That(json, Is.EqualTo(expectedJson));
Assert.Equal(expectedJson, json);
}

[Test]
[Fact]
public void GivenAUserWithEmailAddressJson_WhenUsingEmailAddressConverter_ThenCanDeserializeItIntoUser()
{
const string json = "{\"EmailAddress\":\"email@example.com\"}";
var settings = new JsonSerializerOptions {Converters = {new EmailAddressConverter()}};
var settings = new JsonSerializerOptions { Converters = { new EmailAddressConverter() } };
var user = JsonSerializer.Deserialize<User>(json, settings);
Assert.That(user, Is.Not.Null);
Assert.That(user!.EmailAddress, Is.Not.Null);
Assert.That(user.EmailAddress.Address, Is.EqualTo("email@example.com"));

Assert.NotNull(user);
Assert.NotNull(user.EmailAddress);
Assert.Equal("email@example.com", user.EmailAddress.Address);
}
}
}
10 changes: 4 additions & 6 deletions domain-driven design/ValueObjects/Tests/Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

Expand All @@ -10,11 +10,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
<PackageReference Include="NUnit.Analyzers" Version="3.6.1" />
<PackageReference Include="coverlet.collector" Version="3.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
<PackageReference Include="xunit.v3.mtp-off" Version="4.0.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion domain-driven design/ValueObjects/Tests/Usings.cs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
global using NUnit.Framework;
global using Xunit;
40 changes: 20 additions & 20 deletions domain-driven design/ValueObjects/Tests/ValueEqualityUnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,35 @@ namespace Tests;

public class ValueEqualityUnitTest
{
[Test]
[Fact]
public void GivenTwoRecords_WhenTheyHaveTheSameData_ThenTheyShouldBeEqual()
{
var hundredUsd = Money.Create(100, "USD");
var another100Usd = Money.Create(100, "USD");
Assert.That(hundredUsd.IsSuccess, Is.True);
Assert.That(another100Usd.IsSuccess, Is.True);
Assert.That(another100Usd.Value, Is.EqualTo(hundredUsd.Value));

Assert.True(hundredUsd.IsSuccess);
Assert.True(another100Usd.IsSuccess);
Assert.Equal(hundredUsd.Value, another100Usd.Value);
}
[Test]
public void GivenTwoRecords_WhenTheyHaveTheDifferentData_ThenTheyShouldNotBeEqual()

[Fact]
public void GivenTwoRecords_WhenTheyHaveDifferentData_ThenTheyShouldNotBeEqual()
{
var hundredUsd = Money.Create(100, "USD");
var hundredEur = Money.Create(100, "EUR");
Assert.That(hundredUsd.IsSuccess, Is.True);
Assert.That(hundredEur.IsSuccess, Is.True);
Assert.That(hundredEur.Value, Is.Not.EqualTo(hundredUsd.Value));

Assert.True(hundredUsd.IsSuccess);
Assert.True(hundredEur.IsSuccess);
Assert.NotEqual(hundredUsd.Value, hundredEur.Value);
}
[Test]

[Fact]
public void GivenTwoClasses_WhenTheyHaveTheSameData_ThenTheyShouldNotBeEqual()
{
var hundredUsd = Money.Create(100, "USD");
var payment1 = new Payment(hundredUsd.Value!);
var payment2 = new Payment(hundredUsd.Value!);
Assert.That(payment1, Is.Not.EqualTo(payment2));
var hundredUsd = Money.Create(100, "USD").Value;
var payment1 = new Payment(hundredUsd);
var payment2 = new Payment(hundredUsd);

Assert.NotEqual(payment1, payment2);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using ValueObjects.ValueObjects;
using ValueObjects.ValueObjects;

namespace ValueObjects.Entities;

Expand All @@ -14,4 +14,9 @@ public Payment(Money quoted)
Quoted = quoted;
CreatedAt = DateTime.UtcNow;
}
}

private Payment()
{
Quoted = null!;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore;
using ValueObjects.Entities;

namespace ValueObjects.Persistence;

public sealed class PaymentsDbContext(DbContextOptions<PaymentsDbContext> options)
: DbContext(options)
{
public DbSet<Payment> Payments => Set<Payment>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Payment>().ComplexProperty(p => p.Quoted, money =>
{
money.Property(m => m.Amount);
money.Property(m => m.Currency).HasMaxLength(3);
});
}
}
60 changes: 60 additions & 0 deletions domain-driven design/ValueObjects/ValueObjects/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using ValueObjects.Entities;
using ValueObjects.Persistence;
using ValueObjects.TypeSafety;
using ValueObjects.ValueObjects;

var refused = Money.Create(-5m, "USD");

Console.WriteLine(refused.IsSuccess);
Console.WriteLine(refused.Error);

var originCountry = new Country("US", "United States of America");
var originStation = new Station("JFK", "John F. Kennedy International Airport");
var destinationCountry = new Country("CA", "Canada");
var destinationStation = new Station("YVR", "Vancouver International Airport");

Console.WriteLine(new TicketPriceProvider().GetTicketPrice(originCountry, originStation, destinationCountry, destinationStation));

var hundredUsd = Money.Create(100m, "USD").Value;
var typedInLowerCase = Money.Create(100m, "usd").Value;
var hundredEur = Money.Create(100m, "EUR").Value;

Console.WriteLine(hundredUsd == typedInLowerCase);
Console.WriteLine(hundredUsd == hundredEur);
Console.WriteLine(new Payment(hundredUsd) == new Payment(hundredUsd));

await using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();

var options = new DbContextOptionsBuilder<PaymentsDbContext>()
.UseSqlite(connection)
.Options;

await using (var context = new PaymentsDbContext(options))
{
await context.Database.EnsureCreatedAsync();
Console.WriteLine(context.Database.GenerateCreateScript().TrimEnd());

context.Payments.Add(new Payment(typedInLowerCase));
await context.SaveChangesAsync();
}

await using (var context = new PaymentsDbContext(options))
{
var price = Money.Create(100m, "USD").Value;
var query = context.Payments.Where(p => p.Quoted == price);

Console.WriteLine(query.ToQueryString());

var payment = await query.SingleAsync();

Console.WriteLine(payment.Quoted);
Console.WriteLine(payment.Quoted == hundredUsd);
}

Station[] stops = [new("JFK", "John F. Kennedy International Airport"), new("YVR", "Vancouver International Airport")];
Station[] sameStops = [new("JFK", "John F. Kennedy International Airport"), new("YVR", "Vancouver International Airport")];

Console.WriteLine(new Route(stops) == new Route(sameStops));
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ public class TicketPriceProvider : ITicketPriceProvider
{
public Money GetTicketPrice(Country originCountry, Station originStation, Country destinationCountry, Station destinationStation)
{
return Money.Create(100, "USD").Value!;
return Money.Create(100, "USD").Value;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.12" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
namespace ValueObjects.ValueObjects;

public record EmailAddress(string Address);
public record EmailAddress(string Address); //validation omitted for brevity
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
namespace ValueObjects.ValueObjects;

public record Money
public sealed record Money
{
private static readonly IReadOnlyCollection<string> SupportedCurrencies = new[]{"USD", "EUR"};
private static readonly string[] SupportedCurrencies = ["USD", "EUR"];

public decimal Amount { get; }
public string Currency { get; }

private Money(decimal amount, string currency)
{
Amount = amount;
Expand All @@ -15,12 +15,14 @@ private Money(decimal amount, string currency)

public static Result<Money> Create(decimal amount, string currency)
{
if(string.IsNullOrWhiteSpace(currency))
return Result<Money>.Failure($"{nameof(currency)} cannot be null or whitespace.");
if (amount < 0)
return MoneyErrors.NegativeAmount(amount);

var code = currency.ToUpperInvariant();

if (!SupportedCurrencies.Contains(code))
return MoneyErrors.UnsupportedCurrency(currency);

if(!SupportedCurrencies.Contains(currency.ToUpperInvariant()))
return Result<Money>.Failure($"'{currency}' is not supported.");

return Result<Money>.Success(new(amount, currency));
return new Money(amount, code);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace ValueObjects.ValueObjects;

public static class MoneyErrors
{
public static Error NegativeAmount(decimal amount) =>
new("Money.NegativeAmount", $"Amount {amount} is negative.", ErrorType.Validation);

public static Error UnsupportedCurrency(string currency) =>
new("Money.UnsupportedCurrency", $"Currency '{currency}' is not supported.", ErrorType.Validation);
}
Loading
Loading