Skip to content

Commit

Permalink
Minor refactorings
Browse files Browse the repository at this point in the history
  • Loading branch information
jezzsantos committed Jan 14, 2024
1 parent 6c6f135 commit 832d09f
Show file tree
Hide file tree
Showing 13 changed files with 116 additions and 40 deletions.
6 changes: 3 additions & 3 deletions docs/design-principles/0050-domain-driven-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,10 +359,10 @@ For example, these two use-cases. The first use case is as simple as it gets. Th
}

// Note: Raises a new domain event called UnavailabilitySlotAdded, this can also fail if the invariants of the aggregate fail, which is run after immediately the event is raised.
var raiseEvent = RaiseChangeEvent(Car.UnavailabilitySlotAdded.Create(Id, OrganizationId, slot, causedBy.Value));
if (!raiseEvent.IsSuccessful)
var raised = RaiseChangeEvent(Car.UnavailabilitySlotAdded.Create(Id, OrganizationId, slot, causedBy.Value));
if (!raised.IsSuccessful)
{
return raiseEvent.Error;
return raised.Error;
}

// Note: This use case returns a result, which the caller can use to decide what to do
Expand Down
45 changes: 45 additions & 0 deletions src/BookingsApplication.UnitTests/BookingsApplicationSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,51 @@ await _application.MakeBookingAsync(_caller.Object, "anorganizationid", "acarid"
), It.IsAny<CancellationToken>()));
}

[Fact]
public async Task WhenMakeBookingAsyncAndHasNoEndDateAndHasAvailability_ThenReturnsBooking()
{
_carsService.Setup(cs => cs.GetCarAsync(It.IsAny<ICallerContext>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<Result<Car, Error>>(new Car
{
Id = "acarid",
Managers = null,
Manufacturer = null,
Owner = null,
Plate = null,
Status = "astatus"
}));
var start = DateTime.UtcNow;
_carsService.Setup(
cs => cs.ReserveCarIfAvailableAsync(It.IsAny<ICallerContext>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<DateTime>(), It.IsAny<DateTime>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<Result<bool, Error>>(true));

var result =
await _application.MakeBookingAsync(_caller.Object, "anorganizationid", "acarid", start, null,
CancellationToken.None);

var expectedEnd = start.Add(BookingsApplication.DefaultBookingDuration);
result.Should().BeSuccess();
result.Value.Id.Should().Be("anid");
result.Value.CarId.Should().Be("acarid");
result.Value.BorrowerId.Should().Be("acallerid");
result.Value.EndUtc.Should().Be(expectedEnd);
result.Value.StartUtc.Should().Be(start);
_carsService.Verify(
cs => cs.ReserveCarIfAvailableAsync(_caller.Object
, "anorganizationid", "acarid",
start, expectedEnd, "anid", It.IsAny<CancellationToken>()));
_repository.Verify(rep => rep.SaveAsync(It.Is<BookingRoot>(booking =>
booking.Id == "anid"
&& booking.OrganizationId == "anorganizationid"
&& booking.CarId == "acarid".ToId()
&& booking.BorrowerId == "acallerid".ToId()
&& booking.Start == start
&& booking.End == expectedEnd
), It.IsAny<CancellationToken>()));
}

[Fact]
public async Task WhenSearchAllBookingsAsync_ThenReturnsBookings()
{
Expand Down
2 changes: 1 addition & 1 deletion src/BookingsApplication/BookingsApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace BookingsApplication;

public class BookingsApplication : IBookingsApplication
{
private static readonly TimeSpan DefaultBookingDuration = TimeSpan.FromHours(1);
public static readonly TimeSpan DefaultBookingDuration = TimeSpan.FromHours(1);
private readonly ICarsService _carsService;
private readonly IIdentifierFactory _idFactory;
private readonly IRecorder _recorder;
Expand Down
3 changes: 1 addition & 2 deletions src/BookingsInfrastructure/Api/Bookings/BookingsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ public async Task<ApiPostResult<Booking, MakeBookingResponse>> Make(MakeBookingR
CancellationToken cancellationToken)
{
var booking = await _bookingsApplication.MakeBookingAsync(_contextFactory.Create(), OrganizationId,
request.CarId,
request.StartUtc, request.EndUtc, cancellationToken);
request.CarId, request.StartUtc, request.EndUtc, cancellationToken);

return () => booking.HandleApplicationResult<MakeBookingResponse, Booking>(c =>
new PostResult<MakeBookingResponse>(new MakeBookingResponse { Booking = c }));
Expand Down
25 changes: 13 additions & 12 deletions src/CarsApplication/CarsApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,24 +203,25 @@ public async Task<Result<bool, Error>> ReserveCarIfAvailableAsync(ICallerContext
return slot.Error;
}

var available = car.ReserveIfAvailable(slot.Value, referenceId);
if (!available.IsSuccessful)
var availability = car.ReserveIfAvailable(slot.Value, referenceId);
if (!availability.IsSuccessful)
{
return available.Error;
return availability.Error;
}

if (available.Value)
var isAvailable = availability.Value;
if (!isAvailable)
{
var updated = await _repository.SaveAsync(car, cancellationToken);
return updated.Match<Result<bool, Error>>(_ =>
{
_recorder.TraceInformation(caller.ToCall(), "Car {Id} was made reserved from {From} until {To}",
car.Id, fromUtc, toUtc);
return available.Value;
}, error => error);
return false;
}

return available.Value;
var updated = await _repository.SaveAsync(car, cancellationToken);
return updated.Match<Result<bool, Error>>(_ =>
{
_recorder.TraceInformation(caller.ToCall(), "Car {Id} was reserved from {From} until {To}",
car.Id, fromUtc, toUtc);
return true;
}, error => error);
}

public async Task<Result<SearchResults<Car>, Error>> SearchAllAvailableCarsAsync(ICallerContext caller,
Expand Down
12 changes: 12 additions & 0 deletions src/CarsDomain.UnitTests/CarRootSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,18 @@ public void WhenReserveIfAvailableInThePast_ThenReturnsError()
result.Should().BeError(ErrorCode.Validation, Resources.CarRoot_ReserveInPast);
}

[Fact]
public void WhenReserveIfAvailableWithoutAReference_ThenReturnsError()
{
var start = DateTime.UtcNow.AddSeconds(1);
var end = start.AddHours(1);
SetupCar();

var result = _car.ReserveIfAvailable(TimeSlot.Create(start, end).Value, Optional<string>.None);

result.Should().BeError(ErrorCode.Validation, Resources.CarRoot_ReferenceMissing);
}

#if TESTINGONLY
[Fact]
public void WhenReserveIfAvailableAndUnavailable_ThenReturnsFalse()
Expand Down
18 changes: 12 additions & 6 deletions src/CarsDomain/CarRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,18 @@ public Result<Error> ReleaseUnavailability(TimeSlot slot)
return Result.Ok;
}

public Result<bool, Error> ReserveIfAvailable(TimeSlot slot, string referenceId)
public Result<bool, Error> ReserveIfAvailable(TimeSlot slot, Optional<string> referenceId)
{
if (slot.IsInvalidParameter(s => s.StartsAfter(DateTime.UtcNow), nameof(slot),
Resources.CarRoot_ReserveInPast, out var error))
Resources.CarRoot_ReserveInPast, out var error1))
{
return error;
return error1;
}

if (referenceId.IsInvalidParameter(r => r.HasValue, nameof(referenceId),
Resources.CarRoot_ReferenceMissing, out var error2))
{
return error2;
}

if (!IsAvailable(slot))
Expand All @@ -226,12 +232,12 @@ public Result<bool, Error> ReserveIfAvailable(TimeSlot slot, string referenceId)
return causedBy.Error;
}

var raiseEvent =
var raised =
RaiseChangeEvent(
CarsDomain.Events.UnavailabilitySlotAdded.Create(Id, OrganizationId, slot, causedBy.Value));
if (!raiseEvent.IsSuccessful)
if (!raised.IsSuccessful)
{
return raiseEvent.Error;
return raised.Error;
}

return true;
Expand Down
10 changes: 5 additions & 5 deletions src/CarsDomain/CausedBy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ namespace CarsDomain;

public sealed class CausedBy : ValueObjectBase<CausedBy>
{
public static Result<CausedBy, Error> Create(UnavailabilityCausedBy reason, string? reference)
public static Result<CausedBy, Error> Create(UnavailabilityCausedBy reason, Optional<string> reference)
{
if (reference.Exists())
if (reference.HasValue)
{
if (reference.IsInvalidParameter(Validations.Unavailability.Reference, nameof(reference),
if (reference.Value.IsInvalidParameter(Validations.Unavailability.Reference, nameof(reference),
Resources.CausedBy_InvalidReference, out var error1))
{
return error1;
Expand All @@ -31,10 +31,10 @@ public static Result<CausedBy, Error> Create(UnavailabilityCausedBy reason, stri
return new CausedBy(reason, reference);
}

private CausedBy(UnavailabilityCausedBy reason, string? reference)
private CausedBy(UnavailabilityCausedBy reason, Optional<string> reference)
{
Reason = reason;
Reference = reference;
Reference = reference.ValueOrDefault;
}

public UnavailabilityCausedBy Reason { get; }
Expand Down
9 changes: 9 additions & 0 deletions src/CarsDomain/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/CarsDomain/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@
<data name="CarRoot_ReserveInPast" xml:space="preserve">
<value>The car cannot be reserved in the past</value>
</data>
<data name="CarRoot_ReferenceMissing" xml:space="preserve">
<value>The car cannot be reserved without a reference</value>
</data>
<data name="CarRoot_Unavailable" xml:space="preserve">
<value>The car is not available for this time slot</value>
</data>
Expand Down
16 changes: 8 additions & 8 deletions src/Common/Extensions/ObjectExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static bool Exists([NotNullWhen(true)] this object? instance)
/// Whether the parameter <see cref="value" /> from being invalid according to the <see cref="validation" />,
/// and if invalid, returns a <see cref="ErrorCode.Validation" /> error
/// </summary>
public static bool IsInvalidParameter<TValue>(this TValue? value, Func<TValue, bool> validator,
public static bool IsInvalidParameter<TValue>(this TValue? value, Predicate<TValue> predicate,
string parameterName, string? errorMessage, out Error error)
{
if (value.NotExists())
Expand All @@ -41,14 +41,14 @@ public static bool IsInvalidParameter<TValue>(this TValue? value, Func<TValue, b
return true;
}

return IsInvalidParameter(() => validator(value), parameterName, errorMessage, out error);
return IsInvalidParameter(() => predicate(value), parameterName, errorMessage, out error);
}

/// <summary>
/// Whether the parameter <see cref="value" /> from being invalid according to the <see cref="validation" />,
/// and if invalid, returns a <see cref="ErrorCode.Validation" /> error
/// </summary>
public static bool IsInvalidParameter<TValue>(this TValue? value, Func<TValue, bool> validator,
public static bool IsInvalidParameter<TValue>(this TValue? value, Predicate<TValue> predicate,
string parameterName, out Error error)
{
if (value.NotExists())
Expand All @@ -57,7 +57,7 @@ public static bool IsInvalidParameter<TValue>(this TValue? value, Func<TValue, b
return true;
}

return IsInvalidParameter(() => validator(value), parameterName, null, out error);
return IsInvalidParameter(() => predicate(value), parameterName, null, out error);
}

/// <summary>
Expand Down Expand Up @@ -113,10 +113,10 @@ public static void PopulateWith<TType>(this TType target, IReadOnlyDictionary<st
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException" /> if the specified <see cref="value" /> is invalid
/// </summary>
public static void ThrowIfInvalidParameter<TValue>(this TValue? value, Func<TValue?, bool> validator,
public static void ThrowIfInvalidParameter<TValue>(this TValue? value, Predicate<TValue?> predicate,
string parameterName, string? errorMessage = null)
{
if (value.IsInvalidParameter(validator, parameterName, errorMessage, out _))
if (value.IsInvalidParameter(predicate, parameterName, errorMessage, out _))
{
throw new ArgumentOutOfRangeException(parameterName, errorMessage);
}
Expand All @@ -133,10 +133,10 @@ public static void ThrowIfNotValuedParameter(this string? value, string paramete
}
}

private static bool IsInvalidParameter(Func<bool> validationFunc, string parameterName, string? errorMessage,
private static bool IsInvalidParameter(Func<bool> predicate, string parameterName, string? errorMessage,
out Error error)
{
var isValid = validationFunc();
var isValid = predicate();
if (!isValid)
{
error = errorMessage.HasValue()
Expand Down
6 changes: 3 additions & 3 deletions src/Domain.Common/Entities/AggregateRootBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -363,11 +363,11 @@ protected Result<Error> RaiseEventToChildEntity<TEntity, TChangeEvent>(TChangeEv
/// </summary>
protected Result<Error> RaisePermanentDeleteEvent(Identifier deletedById)
{
var raiseEvent = RaiseEvent(Global.StreamDeleted.Create(Identifier.Create(Id), deletedById), false,
var raised = RaiseEvent(Global.StreamDeleted.Create(Identifier.Create(Id), deletedById), false,
false);
if (!raiseEvent.IsSuccessful)
if (!raised.IsSuccessful)
{
return raiseEvent;
return raised;
}

IsDeleted = true;
Expand Down
1 change: 1 addition & 0 deletions src/SaaStack.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Int64 x:Key="/Default/Environment/UnitTesting/ParallelProcessesCount/@EntryValue">10</s:Int64>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0C41D02830B426419B2E0D9AF25191F6/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0C41D02830B426419B2E0D9AF25191F6/Applicability/=Live/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0C41D02830B426419B2E0D9AF25191F6/Description/@EntryValue">A DDD entity</s:String>
Expand Down

0 comments on commit 832d09f

Please sign in to comment.