diff --git a/docs/design-principles/0050-domain-driven-design.md b/docs/design-principles/0050-domain-driven-design.md index b6b8b7c7..568b127c 100644 --- a/docs/design-principles/0050-domain-driven-design.md +++ b/docs/design-principles/0050-domain-driven-design.md @@ -506,7 +506,7 @@ An entity class should derive from `EntityBase,` and the designer should decide For example, ```c# -public sealed class UnavailabilityEntity : EntityBase +public sealed class Unavailability : EntityBase { } ``` @@ -640,10 +640,10 @@ For example, ```c# // Note: The attribute below identifies the 'Trip' table/container in the persistence store [EntityName("Trip")] -public sealed class TripEntity : EntityBase +public sealed class Trip : EntityBase { // Note: This ctor must populate the entire internal state of the whole entity using properties that were rehydrated from the persistence store. Called by the 'Rehydrate()' method below - private TripEntity(Identifier identifier, IDependencyContainer container, + private Trip(Identifier identifier, IDependencyContainer container, IReadOnlyDictionary rehydratingProperties) : base( identifier, container, rehydratingProperties) { @@ -656,9 +656,9 @@ public sealed class TripEntity : EntityBase } // Note: This method is called by the runtime when the entity is loaded from a persistence store - public static EntityFactory Rehydrate() + public static EntityFactory Rehydrate() { - return (identifier, container, properties) => new TripEntity(identifier, container, properties); + return (identifier, container, properties) => new Trip(identifier, container, properties); } // Note: this method is called by the runtime when the entity is saved to a persistence store @@ -683,10 +683,10 @@ public sealed class TripEntity : EntityBase For example, ```c# -public sealed class UnavailabilityEntity : EntityBase +public sealed class Unavailability : EntityBase { // Note: This ctor only calls the base class. Called by the 'Create()' factory method - private UnavailabilityEntity(IRecorder recorder, IIdentifierFactory idFactory, RootEventHandler rootEventHandler) : base(recorder, idFactory, rootEventHandler) + private Unavailability(IRecorder recorder, IIdentifierFactory idFactory, RootEventHandler rootEventHandler) : base(recorder, idFactory, rootEventHandler) { } ``` @@ -714,7 +714,7 @@ For example, { if (BeganAt.HasValue) { - return Error.RuleViolation(Resources.TripEntity_AlreadyBegan); + return Error.RuleViolation(Resources.Trip_AlreadyBegan); } var starts = DateTime.UtcNow; @@ -742,17 +742,17 @@ For example: if (BeganAt.HasValue && From.NotExists()) { - return Error.RuleViolation(Resources.TripEntity_NoStartingLocation); + return Error.RuleViolation(Resources.Trip_NoStartingLocation); } if (EndedAt.HasValue && !BeganAt.HasValue) { - return Error.RuleViolation(Resources.TripEntity_NotBegun); + return Error.RuleViolation(Resources.Trip_NotBegun); } if (EndedAt.HasValue && To.NotExists()) { - return Error.RuleViolation(Resources.TripEntity_NoEndingLocation); + return Error.RuleViolation(Resources.Trip_NoEndingLocation); } return Result.Ok; diff --git a/src/AncillaryInfrastructure/AncillaryModule.cs b/src/AncillaryInfrastructure/AncillaryModule.cs index 89509936..5768bae8 100644 --- a/src/AncillaryInfrastructure/AncillaryModule.cs +++ b/src/AncillaryInfrastructure/AncillaryModule.cs @@ -20,13 +20,13 @@ namespace AncillaryInfrastructure; -public class AncillaryModule : ISubDomainModule +public class AncillaryModule : ISubdomainModule { - public Assembly ApiAssembly => typeof(UsagesApi).Assembly; + public Assembly InfrastructureAssembly => typeof(UsagesApi).Assembly; public Assembly DomainAssembly => typeof(AuditRoot).Assembly; - public Dictionary AggregatePrefixes => new() + public Dictionary EntityPrefixes => new() { { typeof(AuditRoot), "audit" }, { typeof(EmailDeliveryRoot), "emaildelivery" } diff --git a/src/ApiHost1/ApiHostModule.cs b/src/ApiHost1/ApiHostModule.cs index 9675d50b..125d35e2 100644 --- a/src/ApiHost1/ApiHostModule.cs +++ b/src/ApiHost1/ApiHostModule.cs @@ -7,13 +7,13 @@ namespace ApiHost1; /// /// Provides a module for common services of a API host /// -public class ApiHostModule : ISubDomainModule +public class ApiHostModule : ISubdomainModule { - public Assembly ApiAssembly => typeof(HealthApi).Assembly; + public Assembly InfrastructureAssembly => typeof(HealthApi).Assembly; - public Assembly DomainAssembly => null!; + public Assembly? DomainAssembly => null; - public Dictionary AggregatePrefixes => new(); + public Dictionary EntityPrefixes => new(); public Action> ConfigureMiddleware { diff --git a/src/ApiHost1/HostedModules.cs b/src/ApiHost1/HostedModules.cs index 09f6c384..8e2e7627 100644 --- a/src/ApiHost1/HostedModules.cs +++ b/src/ApiHost1/HostedModules.cs @@ -10,9 +10,9 @@ namespace ApiHost1; public static class HostedModules { - public static SubDomainModules Get() + public static SubdomainModules Get() { - var modules = new SubDomainModules(); + var modules = new SubdomainModules(); modules.Register(new ApiHostModule()); modules.Register(new EndUsersModule()); modules.Register(new OrganizationsModule()); diff --git a/src/ApiHost1/TestingOnlyApiModule.cs b/src/ApiHost1/TestingOnlyApiModule.cs index 6a4343ec..ed155eea 100644 --- a/src/ApiHost1/TestingOnlyApiModule.cs +++ b/src/ApiHost1/TestingOnlyApiModule.cs @@ -5,13 +5,13 @@ namespace ApiHost1; -public class TestingOnlyApiModule : ISubDomainModule +public class TestingOnlyApiModule : ISubdomainModule { - public Assembly ApiAssembly => typeof(TestingWebApi).Assembly; + public Assembly InfrastructureAssembly => typeof(TestingWebApi).Assembly; - public Assembly DomainAssembly => null!; + public Assembly? DomainAssembly => null; - public Dictionary AggregatePrefixes => new(); + public Dictionary EntityPrefixes => new(); public Action> ConfigureMiddleware { diff --git a/src/BookingsDomain.UnitTests/TripEntitySpec.cs b/src/BookingsDomain.UnitTests/TripSpec.cs similarity index 94% rename from src/BookingsDomain.UnitTests/TripEntitySpec.cs rename to src/BookingsDomain.UnitTests/TripSpec.cs index 954fb157..084fd571 100644 --- a/src/BookingsDomain.UnitTests/TripEntitySpec.cs +++ b/src/BookingsDomain.UnitTests/TripSpec.cs @@ -10,16 +10,16 @@ namespace BookingsDomain.UnitTests; [Trait("Category", "Unit")] -public class TripEntitySpec +public class TripSpec { - private readonly TripEntity _trip; + private readonly Trip _trip; - public TripEntitySpec() + public TripSpec() { var recorder = new Mock(); var idFactory = new FixedIdentifierFactory("anid"); - _trip = TripEntity.Create(recorder.Object, idFactory, _ => Result.Ok).Value; + _trip = Trip.Create(recorder.Object, idFactory, _ => Result.Ok).Value; _trip.RaiseChangeEvent(Events.TripAdded.Create("arootid".ToId(), "anorganizationid".ToId())); } diff --git a/src/BookingsDomain.UnitTests/TripsSpec.cs b/src/BookingsDomain.UnitTests/TripsSpec.cs index cea53fd6..33984e63 100644 --- a/src/BookingsDomain.UnitTests/TripsSpec.cs +++ b/src/BookingsDomain.UnitTests/TripsSpec.cs @@ -16,7 +16,7 @@ public void WhenAdd_ThenAddsTrip() { var recorder = new Mock(); var idFactory = new FixedIdentifierFactory("anid"); - var trip = TripEntity.Create(recorder.Object, idFactory, _ => Result.Ok).Value; + var trip = Trip.Create(recorder.Object, idFactory, _ => Result.Ok).Value; _trips.Add(trip); @@ -37,7 +37,7 @@ public void WhenLatestAndSome_ThenReturnsLast() { var recorder = new Mock(); var idFactory = new FixedIdentifierFactory("anid"); - var trip = TripEntity.Create(recorder.Object, idFactory, _ => Result.Ok).Value; + var trip = Trip.Create(recorder.Object, idFactory, _ => Result.Ok).Value; _trips.Add(trip); var result = _trips.Latest(); diff --git a/src/BookingsDomain/BookingRoot.cs b/src/BookingsDomain/BookingRoot.cs index 318407f5..441ed94b 100644 --- a/src/BookingsDomain/BookingRoot.cs +++ b/src/BookingsDomain/BookingRoot.cs @@ -114,7 +114,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco case Events.TripAdded changed: { var trip = RaiseEventToChildEntity(isReconstituting, changed, idFactory => - TripEntity.Create(Recorder, idFactory, RaiseChangeEvent), e => e.TripId!); + Trip.Create(Recorder, idFactory, RaiseChangeEvent), e => e.TripId!); if (!trip.IsSuccessful) { return trip.Error; diff --git a/src/BookingsDomain/TripEntity.cs b/src/BookingsDomain/Trip.cs similarity index 89% rename from src/BookingsDomain/TripEntity.cs rename to src/BookingsDomain/Trip.cs index f58c1c62..ebfc3424 100644 --- a/src/BookingsDomain/TripEntity.cs +++ b/src/BookingsDomain/Trip.cs @@ -12,20 +12,20 @@ namespace BookingsDomain; [EntityName("Trip")] -public sealed class TripEntity : EntityBase +public sealed class Trip : EntityBase { - public static Result Create(IRecorder recorder, IIdentifierFactory idFactory, + public static Result Create(IRecorder recorder, IIdentifierFactory idFactory, RootEventHandler rootEventHandler) { - return new TripEntity(recorder, idFactory, rootEventHandler); + return new Trip(recorder, idFactory, rootEventHandler); } - private TripEntity(IRecorder recorder, IIdentifierFactory idFactory, + private Trip(IRecorder recorder, IIdentifierFactory idFactory, RootEventHandler rootEventHandler) : base(recorder, idFactory, rootEventHandler) { } - private TripEntity(ISingleValueObject identifier, IDependencyContainer container, + private Trip(ISingleValueObject identifier, IDependencyContainer container, HydrationProperties rehydratingProperties) : base(identifier, container, rehydratingProperties) { RootId = rehydratingProperties.GetValueOrDefault(nameof(RootId)); @@ -48,9 +48,9 @@ private TripEntity(ISingleValueObject identifier, IDependencyContainer c public Optional To { get; private set; } - public static EntityFactory Rehydrate() + public static EntityFactory Rehydrate() { - return (identifier, container, properties) => new TripEntity(identifier, container, properties); + return (identifier, container, properties) => new Trip(identifier, container, properties); } public override HydrationProperties Dehydrate() diff --git a/src/BookingsDomain/Trips.cs b/src/BookingsDomain/Trips.cs index d002f3db..b6a63a89 100644 --- a/src/BookingsDomain/Trips.cs +++ b/src/BookingsDomain/Trips.cs @@ -2,11 +2,11 @@ namespace BookingsDomain; -public class Trips : IReadOnlyList +public class Trips : IReadOnlyList { - private readonly List _trips = new(); + private readonly List _trips = new(); - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { return _trips.GetEnumerator(); } @@ -18,14 +18,14 @@ IEnumerator IEnumerable.GetEnumerator() public int Count => _trips.Count; - public TripEntity this[int index] => _trips[index]; + public Trip this[int index] => _trips[index]; - public void Add(TripEntity trip) + public void Add(Trip trip) { _trips.Add(trip); } - public TripEntity? Latest() + public Trip? Latest() { return _trips.LastOrDefault(); } diff --git a/src/BookingsInfrastructure/BookingsModule.cs b/src/BookingsInfrastructure/BookingsModule.cs index 3492b095..cfacd2fc 100644 --- a/src/BookingsInfrastructure/BookingsModule.cs +++ b/src/BookingsInfrastructure/BookingsModule.cs @@ -12,16 +12,16 @@ namespace BookingsInfrastructure; -public class BookingsModule : ISubDomainModule +public class BookingsModule : ISubdomainModule { - public Assembly ApiAssembly => typeof(BookingsApi).Assembly; + public Assembly InfrastructureAssembly => typeof(BookingsApi).Assembly; public Assembly DomainAssembly => typeof(BookingRoot).Assembly; - public Dictionary AggregatePrefixes => new() + public Dictionary EntityPrefixes => new() { { typeof(BookingRoot), "booking" }, - { typeof(TripEntity), "trip" } + { typeof(Trip), "trip" } }; public Action> ConfigureMiddleware diff --git a/src/CarsApplication/CarsApplication.cs b/src/CarsApplication/CarsApplication.cs index 8b3b3ed7..83014023 100644 --- a/src/CarsApplication/CarsApplication.cs +++ b/src/CarsApplication/CarsApplication.cs @@ -7,6 +7,7 @@ using Common.Extensions; using Domain.Common.Identity; using Domain.Common.ValueObjects; +using Unavailability = Application.Resources.Shared.Unavailability; namespace CarsApplication; diff --git a/src/CarsApplication/Persistence/ICarRepository.cs b/src/CarsApplication/Persistence/ICarRepository.cs index fe15ced5..3103c2ff 100644 --- a/src/CarsApplication/Persistence/ICarRepository.cs +++ b/src/CarsApplication/Persistence/ICarRepository.cs @@ -4,6 +4,7 @@ using CarsDomain; using Common; using Domain.Common.ValueObjects; +using Unavailability = CarsApplication.Persistence.ReadModels.Unavailability; namespace CarsApplication.Persistence; diff --git a/src/CarsDomain.UnitTests/CarRootSpec.cs b/src/CarsDomain.UnitTests/CarRootSpec.cs index 7e7488b1..684de66e 100644 --- a/src/CarsDomain.UnitTests/CarRootSpec.cs +++ b/src/CarsDomain.UnitTests/CarRootSpec.cs @@ -24,7 +24,7 @@ public CarRootSpec() identifierFactory.Setup(f => f.Create(It.IsAny())) .Returns((IIdentifiableEntity e) => { - if (e is UnavailabilityEntity) + if (e is Unavailability) { return $"anunavailbilityid{++entityCount}".ToId(); } diff --git a/src/CarsDomain.UnitTests/UnavailabilityEntitySpec.cs b/src/CarsDomain.UnitTests/UnavailabilitySpec.cs similarity index 86% rename from src/CarsDomain.UnitTests/UnavailabilityEntitySpec.cs rename to src/CarsDomain.UnitTests/UnavailabilitySpec.cs index 3f902028..ff8d9af2 100644 --- a/src/CarsDomain.UnitTests/UnavailabilityEntitySpec.cs +++ b/src/CarsDomain.UnitTests/UnavailabilitySpec.cs @@ -11,15 +11,15 @@ namespace CarsDomain.UnitTests; [Trait("Category", "Unit")] -public class UnavailabilityEntitySpec +public class UnavailabilitySpec { private readonly DateTime _end; private readonly Mock _idFactory; private readonly Mock _recorder; private readonly DateTime _start; - private readonly UnavailabilityEntity _unavailability; + private readonly Unavailability _unavailability; - public UnavailabilityEntitySpec() + public UnavailabilitySpec() { _recorder = new Mock(); _idFactory = new Mock(); @@ -27,7 +27,7 @@ public UnavailabilityEntitySpec() .Returns("anid".ToId()); _start = DateTime.UtcNow; _end = _start.AddHours(1); - _unavailability = UnavailabilityEntity.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok).Value; + _unavailability = Unavailability.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok).Value; } [Fact] @@ -69,7 +69,7 @@ public void WhenOverlapsAndOverlapping_ThenReturnsTrue() [Fact] public void WhenIsDifferentCauseAndHasNoCausedByInEither_ThenReturnsFalse() { - var other = UnavailabilityEntity.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok); + var other = Unavailability.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok); var result = _unavailability.IsDifferentCause(other.Value); @@ -80,7 +80,7 @@ public void WhenIsDifferentCauseAndHasNoCausedByInEither_ThenReturnsFalse() [Fact] public void WhenIsDifferentCauseAndHasNoCausedInSource_ThenReturnsTrue() { - var other = UnavailabilityEntity.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok); + var other = Unavailability.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok); other.Value.TestingOnly_Assign("acarid".ToId(), "anorganizationid".ToId(), TimeSlot.Create(_start, _end).Value, CausedBy.Create(UnavailabilityCausedBy.Other, null).Value); @@ -96,7 +96,7 @@ public void WhenIsDifferentCauseAndHasNoCausedInOther_ThenReturnsTrue() { _unavailability.TestingOnly_Assign("acarid".ToId(), "anorganizationid".ToId(), TimeSlot.Create(_start, _end).Value, CausedBy.Create(UnavailabilityCausedBy.Other, null).Value); - var other = UnavailabilityEntity.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok); + var other = Unavailability.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok); var result = _unavailability.IsDifferentCause(other.Value); @@ -110,7 +110,7 @@ public void WhenIsDifferentCauseAndHaveSameCausesAndNoReferences_ThenReturnsFals { _unavailability.TestingOnly_Assign("acarid".ToId(), "anorganizationid".ToId(), TimeSlot.Create(_start, _end).Value, CausedBy.Create(UnavailabilityCausedBy.Other, null).Value); - var other = UnavailabilityEntity.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok); + var other = Unavailability.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok); other.Value.TestingOnly_Assign("acarid".ToId(), "anorganizationid".ToId(), TimeSlot.Create(_start, _end).Value, CausedBy.Create(UnavailabilityCausedBy.Other, null).Value); @@ -126,7 +126,7 @@ public void WhenIsDifferentCauseAndHaveSameCausesAndDifferentReferences_ThenRetu { _unavailability.TestingOnly_Assign("acarid".ToId(), "anorganizationid".ToId(), TimeSlot.Create(_start, _end).Value, CausedBy.Create(UnavailabilityCausedBy.Other, "areference1").Value); - var other = UnavailabilityEntity.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok); + var other = Unavailability.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok); other.Value.TestingOnly_Assign("acarid".ToId(), "anorganizationid".ToId(), TimeSlot.Create(_start, _end).Value, CausedBy.Create(UnavailabilityCausedBy.Other, "areference2").Value); @@ -142,7 +142,7 @@ public void WhenIsDifferentCauseAndHaveDifferentCausesAndNullReference_ThenRetur { _unavailability.TestingOnly_Assign("acarid".ToId(), "anorganizationid".ToId(), TimeSlot.Create(_start, _end).Value, CausedBy.Create(UnavailabilityCausedBy.Other, null).Value); - var other = UnavailabilityEntity.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok); + var other = Unavailability.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok); other.Value.TestingOnly_Assign("acarid".ToId(), "anorganizationid".ToId(), TimeSlot.Create(_start, _end).Value, CausedBy.Create(UnavailabilityCausedBy.Maintenance, null).Value); @@ -158,7 +158,7 @@ public void WhenIsDifferentCauseAndHaveDifferentCausesAndSameReference_ThenRetur { _unavailability.TestingOnly_Assign("acarid".ToId(), "anorganizationid".ToId(), TimeSlot.Create(_start, _end).Value, CausedBy.Create(UnavailabilityCausedBy.Other, "areference").Value); - var other = UnavailabilityEntity.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok); + var other = Unavailability.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok); other.Value.TestingOnly_Assign("acarid".ToId(), "anorganizationid".ToId(), TimeSlot.Create(_start, _end).Value, CausedBy.Create(UnavailabilityCausedBy.Maintenance, "areference").Value); @@ -175,7 +175,7 @@ public void WhenIsDifferentCauseAndHaveDifferentCausesAndDifferentReference_Then { _unavailability.TestingOnly_Assign("acarid".ToId(), "anorganizationid".ToId(), TimeSlot.Create(_start, _end).Value, CausedBy.Create(UnavailabilityCausedBy.Other, "areference1").Value); - var other = UnavailabilityEntity.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok); + var other = Unavailability.Create(_recorder.Object, _idFactory.Object, _ => Result.Ok); other.Value.TestingOnly_Assign("acarid".ToId(), "anorganizationid".ToId(), TimeSlot.Create(_start, _end).Value, CausedBy.Create(UnavailabilityCausedBy.Maintenance, "areference2").Value); diff --git a/src/CarsDomain/CarRoot.cs b/src/CarsDomain/CarRoot.cs index e111ee48..e898763f 100644 --- a/src/CarsDomain/CarRoot.cs +++ b/src/CarsDomain/CarRoot.cs @@ -150,7 +150,7 @@ protected override Result OnStateChanged(IDomainEvent @event, bool isReco case Events.UnavailabilitySlotAdded created: { var unavailability = RaiseEventToChildEntity(isReconstituting, created, idFactory => - UnavailabilityEntity.Create(Recorder, idFactory, RaiseChangeEvent), e => e.UnavailabilityId!); + Unavailability.Create(Recorder, idFactory, RaiseChangeEvent), e => e.UnavailabilityId!); if (!unavailability.IsSuccessful) { return unavailability.Error; diff --git a/src/CarsDomain/Unavailabilities.cs b/src/CarsDomain/Unavailabilities.cs index 3ad4bf30..ce38357b 100644 --- a/src/CarsDomain/Unavailabilities.cs +++ b/src/CarsDomain/Unavailabilities.cs @@ -5,9 +5,9 @@ namespace CarsDomain; -public class Unavailabilities : IReadOnlyList +public class Unavailabilities : IReadOnlyList { - private readonly List _unavailabilities = new(); + private readonly List _unavailabilities = new(); public Result EnsureInvariants() { @@ -22,7 +22,7 @@ public Result EnsureInvariants() return Result.Ok; } - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { return _unavailabilities.GetEnumerator(); } @@ -34,9 +34,9 @@ IEnumerator IEnumerable.GetEnumerator() public int Count => _unavailabilities.Count; - public UnavailabilityEntity this[int index] => _unavailabilities[index]; + public Unavailability this[int index] => _unavailabilities[index]; - public void Add(UnavailabilityEntity unavailability) + public void Add(Unavailability unavailability) { var match = FindMatching(unavailability); if (match.Exists()) @@ -47,7 +47,7 @@ public void Add(UnavailabilityEntity unavailability) _unavailabilities.Add(unavailability); } - public UnavailabilityEntity? FindSlot(TimeSlot slot) + public Unavailability? FindSlot(TimeSlot slot) { return _unavailabilities.FirstOrDefault(una => una.Slot.Exists() && una.Slot == slot); } @@ -61,7 +61,7 @@ public void Remove(Identifier unavailabilityId) } } - private UnavailabilityEntity? FindMatching(UnavailabilityEntity unavailability) + private Unavailability? FindMatching(Unavailability unavailability) { return _unavailabilities .FirstOrDefault(u => @@ -75,17 +75,17 @@ private bool HasIncompatibleOverlaps() .Any(next => InConflict(current, next))); } - private static bool IsDifferentFrom(UnavailabilityEntity current, UnavailabilityEntity next) + private static bool IsDifferentFrom(Unavailability current, Unavailability next) { return !next.Equals(current); } - private static bool InConflict(UnavailabilityEntity current, UnavailabilityEntity next) + private static bool InConflict(Unavailability current, Unavailability next) { return Overlaps(current, next) && HasDifferentCause(current, next); } - private static bool Overlaps(UnavailabilityEntity current, UnavailabilityEntity next) + private static bool Overlaps(Unavailability current, Unavailability next) { if (current.Slot.NotExists()) { @@ -100,7 +100,7 @@ private static bool Overlaps(UnavailabilityEntity current, UnavailabilityEntity return next.Slot.IsIntersecting(current.Slot); } - private static bool HasDifferentCause(UnavailabilityEntity current, UnavailabilityEntity next) + private static bool HasDifferentCause(Unavailability current, Unavailability next) { return current.IsDifferentCause(next); } diff --git a/src/CarsDomain/UnavailabilityEntity.cs b/src/CarsDomain/Unavailability.cs similarity index 88% rename from src/CarsDomain/UnavailabilityEntity.cs rename to src/CarsDomain/Unavailability.cs index eeb509c4..ef8387f6 100644 --- a/src/CarsDomain/UnavailabilityEntity.cs +++ b/src/CarsDomain/Unavailability.cs @@ -7,15 +7,15 @@ namespace CarsDomain; -public sealed class UnavailabilityEntity : EntityBase +public sealed class Unavailability : EntityBase { - public static Result Create(IRecorder recorder, IIdentifierFactory idFactory, + public static Result Create(IRecorder recorder, IIdentifierFactory idFactory, RootEventHandler rootEventHandler) { - return new UnavailabilityEntity(recorder, idFactory, rootEventHandler); + return new Unavailability(recorder, idFactory, rootEventHandler); } - private UnavailabilityEntity(IRecorder recorder, IIdentifierFactory idFactory, + private Unavailability(IRecorder recorder, IIdentifierFactory idFactory, RootEventHandler rootEventHandler) : base(recorder, idFactory, rootEventHandler) { } @@ -74,7 +74,7 @@ protected override Result OnStateChanged(IDomainEvent @event) } } - public bool IsDifferentCause(UnavailabilityEntity unavailability) + public bool IsDifferentCause(Unavailability unavailability) { if (CausedBy.NotExists()) { diff --git a/src/CarsInfrastructure/CarsModule.cs b/src/CarsInfrastructure/CarsModule.cs index 92d9e5ce..5312ff99 100644 --- a/src/CarsInfrastructure/CarsModule.cs +++ b/src/CarsInfrastructure/CarsModule.cs @@ -18,16 +18,16 @@ namespace CarsInfrastructure; -public class CarsModule : ISubDomainModule +public class CarsModule : ISubdomainModule { - public Assembly ApiAssembly => typeof(CarsApi).Assembly; + public Assembly InfrastructureAssembly => typeof(CarsApi).Assembly; public Assembly DomainAssembly => typeof(CarRoot).Assembly; - public Dictionary AggregatePrefixes => new() + public Dictionary EntityPrefixes => new() { { typeof(CarRoot), "car" }, - { typeof(UnavailabilityEntity), "unavail" } + { typeof(Unavailability), "unavail" } }; public Action> ConfigureMiddleware diff --git a/src/CarsInfrastructure/Persistence/CarRepository.cs b/src/CarsInfrastructure/Persistence/CarRepository.cs index 0804c158..f06b5431 100644 --- a/src/CarsInfrastructure/Persistence/CarRepository.cs +++ b/src/CarsInfrastructure/Persistence/CarRepository.cs @@ -11,6 +11,7 @@ using Infrastructure.Persistence.Common; using Infrastructure.Persistence.Interfaces; using QueryAny; +using Unavailability = CarsApplication.Persistence.ReadModels.Unavailability; namespace CarsInfrastructure.Persistence; diff --git a/src/CarsInfrastructure/Persistence/ReadModels/CarProjection.cs b/src/CarsInfrastructure/Persistence/ReadModels/CarProjection.cs index eef4c2cf..88afb40a 100644 --- a/src/CarsInfrastructure/Persistence/ReadModels/CarProjection.cs +++ b/src/CarsInfrastructure/Persistence/ReadModels/CarProjection.cs @@ -8,6 +8,7 @@ using Domain.Interfaces.Entities; using Infrastructure.Persistence.Common; using Infrastructure.Persistence.Interfaces; +using Unavailability = CarsApplication.Persistence.ReadModels.Unavailability; namespace CarsInfrastructure.Persistence.ReadModels; diff --git a/src/EndUsersInfrastructure/EndUsersModule.cs b/src/EndUsersInfrastructure/EndUsersModule.cs index 8c4421e5..05d2a009 100644 --- a/src/EndUsersInfrastructure/EndUsersModule.cs +++ b/src/EndUsersInfrastructure/EndUsersModule.cs @@ -21,13 +21,13 @@ namespace EndUsersInfrastructure; -public class EndUsersModule : ISubDomainModule +public class EndUsersModule : ISubdomainModule { - public Assembly ApiAssembly => typeof(EndUsersApi).Assembly; + public Assembly InfrastructureAssembly => typeof(EndUsersApi).Assembly; public Assembly DomainAssembly => typeof(EndUserRoot).Assembly; - public Dictionary AggregatePrefixes => new() + public Dictionary EntityPrefixes => new() { { typeof(EndUserRoot), "user" }, { typeof(Membership), "mship" } diff --git a/src/IdentityInfrastructure/IdentityModule.cs b/src/IdentityInfrastructure/IdentityModule.cs index f076dfa9..d8a88744 100644 --- a/src/IdentityInfrastructure/IdentityModule.cs +++ b/src/IdentityInfrastructure/IdentityModule.cs @@ -27,17 +27,18 @@ namespace IdentityInfrastructure; -public class IdentityModule : ISubDomainModule +public class IdentityModule : ISubdomainModule { - public Assembly ApiAssembly => typeof(PasswordCredentialsApi).Assembly; + public Assembly InfrastructureAssembly => typeof(PasswordCredentialsApi).Assembly; public Assembly DomainAssembly => typeof(PasswordCredentialRoot).Assembly; - public Dictionary AggregatePrefixes => new() + public Dictionary EntityPrefixes => new() { { typeof(PasswordCredentialRoot), "pwdcred" }, { typeof(AuthTokensRoot), "authtok" }, - { typeof(APIKeyRoot), "apikey" } + { typeof(APIKeyRoot), "apikey" }, + { typeof(SSOUserRoot), "ssocred_" } }; public Action> ConfigureMiddleware diff --git a/src/Infrastructure.Web.Hosting.Common.UnitTests/SubDomainModulesSpec.cs b/src/Infrastructure.Web.Hosting.Common.UnitTests/SubdomainModulesSpec.cs similarity index 69% rename from src/Infrastructure.Web.Hosting.Common.UnitTests/SubDomainModulesSpec.cs rename to src/Infrastructure.Web.Hosting.Common.UnitTests/SubdomainModulesSpec.cs index a99a9c4e..8f647970 100644 --- a/src/Infrastructure.Web.Hosting.Common.UnitTests/SubDomainModulesSpec.cs +++ b/src/Infrastructure.Web.Hosting.Common.UnitTests/SubdomainModulesSpec.cs @@ -8,9 +8,9 @@ namespace Infrastructure.Web.Hosting.Common.UnitTests; [Trait("Category", "Unit")] -public class SubDomainModulesSpec +public class SubdomainModulesSpec { - private readonly SubDomainModules _modules = new(); + private readonly SubdomainModules _modules = new(); [Fact] public void WhenRegisterAndNullModule_ThenThrows() @@ -24,8 +24,8 @@ public void WhenRegisterAndNullApiAssembly_ThenThrows() { _modules.Invoking(x => x.Register(new TestModule { - ApiAssembly = null!, - DomainAssembly = typeof(SubDomainModulesSpec).Assembly + InfrastructureAssembly = null!, + DomainAssembly = typeof(SubdomainModulesSpec).Assembly })) .Should().Throw(); } @@ -35,7 +35,7 @@ public void WhenRegisterAndNullDomainAssembly_ThenThrows() { _modules.Invoking(x => x.Register(new TestModule { - ApiAssembly = typeof(SubDomainModulesSpec).Assembly, + InfrastructureAssembly = typeof(SubdomainModulesSpec).Assembly, DomainAssembly = null! })) .Should().Throw(); @@ -46,9 +46,9 @@ public void WhenRegisterAndNullAggregatePrefixes_ThenThrows() { _modules.Invoking(x => x.Register(new TestModule { - AggregatePrefixes = null!, - ApiAssembly = typeof(SubDomainModulesSpec).Assembly, - DomainAssembly = typeof(SubDomainModulesSpec).Assembly + EntityPrefixes = null!, + InfrastructureAssembly = typeof(SubdomainModulesSpec).Assembly, + DomainAssembly = typeof(SubdomainModulesSpec).Assembly })) .Should().Throw(); } @@ -58,9 +58,9 @@ public void WhenRegisterAndNullMinimalApiRegistrationFunction_ThenThrows() { _modules.Invoking(x => x.Register(new TestModule { - AggregatePrefixes = new Dictionary(), - ApiAssembly = typeof(SubDomainModulesSpec).Assembly, - DomainAssembly = typeof(SubDomainModulesSpec).Assembly, + EntityPrefixes = new Dictionary(), + InfrastructureAssembly = typeof(SubdomainModulesSpec).Assembly, + DomainAssembly = typeof(SubdomainModulesSpec).Assembly, RegisterServices = (_, _) => { } })) .Should().Throw(); @@ -71,9 +71,9 @@ public void WhenRegisterAndNullRegisterServicesFunction_ThenRegisters() { _modules.Register(new TestModule { - AggregatePrefixes = new Dictionary(), - ApiAssembly = typeof(SubDomainModulesSpec).Assembly, - DomainAssembly = typeof(SubDomainModulesSpec).Assembly, + EntityPrefixes = new Dictionary(), + InfrastructureAssembly = typeof(SubdomainModulesSpec).Assembly, + DomainAssembly = typeof(SubdomainModulesSpec).Assembly, ConfigureMiddleware = (_, _) => { }, RegisterServices = null! }); @@ -98,9 +98,9 @@ public void WhenRegisterServices_ThenAppliedToAllModules() var wasCalled = false; _modules.Register(new TestModule { - ApiAssembly = typeof(SubDomainModulesSpec).Assembly, - DomainAssembly = typeof(SubDomainModulesSpec).Assembly, - AggregatePrefixes = new Dictionary(), + InfrastructureAssembly = typeof(SubdomainModulesSpec).Assembly, + DomainAssembly = typeof(SubdomainModulesSpec).Assembly, + EntityPrefixes = new Dictionary(), ConfigureMiddleware = (_, _) => { }, RegisterServices = (_, _) => { wasCalled = true; } }); @@ -125,9 +125,9 @@ public void WhenConfigureHost_ThenAppliedToAllModules() var wasCalled = false; _modules.Register(new TestModule { - ApiAssembly = typeof(SubDomainModulesSpec).Assembly, - DomainAssembly = typeof(SubDomainModulesSpec).Assembly, - AggregatePrefixes = new Dictionary(), + InfrastructureAssembly = typeof(SubdomainModulesSpec).Assembly, + DomainAssembly = typeof(SubdomainModulesSpec).Assembly, + EntityPrefixes = new Dictionary(), ConfigureMiddleware = (_, _) => { wasCalled = true; }, RegisterServices = (_, _) => { } }); @@ -138,13 +138,13 @@ public void WhenConfigureHost_ThenAppliedToAllModules() } } -public class TestModule : ISubDomainModule +public class TestModule : ISubdomainModule { - public Assembly ApiAssembly { get; init; } = null!; + public Assembly InfrastructureAssembly { get; init; } = null!; - public Assembly DomainAssembly { get; init; } = null!; + public Assembly? DomainAssembly { get; set; } - public Dictionary AggregatePrefixes { get; init; } = null!; + public Dictionary EntityPrefixes { get; init; } = null!; public Action> ConfigureMiddleware { get; init; } = null!; diff --git a/src/Infrastructure.Web.Hosting.Common/Extensions/HostExtensions.cs b/src/Infrastructure.Web.Hosting.Common/Extensions/HostExtensions.cs index 399c2919..296a3d02 100644 --- a/src/Infrastructure.Web.Hosting.Common/Extensions/HostExtensions.cs +++ b/src/Infrastructure.Web.Hosting.Common/Extensions/HostExtensions.cs @@ -76,7 +76,7 @@ public static class HostExtensions /// /// Configures a WebHost /// - public static WebApplication ConfigureApiHost(this WebApplicationBuilder appBuilder, SubDomainModules modules, + public static WebApplication ConfigureApiHost(this WebApplicationBuilder appBuilder, SubdomainModules modules, WebHostOptions hostOptions) { var services = appBuilder.Services; @@ -366,7 +366,7 @@ void RegisterWireFormats() void RegisterApplicationServices(bool isMultiTenanted) { services.AddHttpClient(); - var prefixes = modules.AggregatePrefixes; + var prefixes = modules.EntityPrefixes; prefixes.Add(typeof(Checkpoint), CheckPointAggregatePrefix); services.AddSingleton(_ => new HostIdentifierFactory(prefixes)); @@ -382,7 +382,7 @@ void RegisterApplicationServices(bool isMultiTenanted) void RegisterPersistence(bool usesQueues, bool isMultiTenanted) { - var domainAssemblies = modules.DomainAssemblies + var domainAssemblies = modules.SubdomainAssemblies .Concat(new[] { typeof(DomainCommonMarker).Assembly, typeof(DomainSharedMarker).Assembly }) .ToArray(); diff --git a/src/Infrastructure.Web.Hosting.Common/ISubDomainModule.cs b/src/Infrastructure.Web.Hosting.Common/ISubDomainModule.cs deleted file mode 100644 index 88501320..00000000 --- a/src/Infrastructure.Web.Hosting.Common/ISubDomainModule.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Reflection; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Infrastructure.Web.Hosting.Common; - -/// -/// Defines a sub domain module -/// -public interface ISubDomainModule -{ - /// - /// Returns the naming prefix for each aggregate - /// - Dictionary AggregatePrefixes { get; } - - /// - /// Returns the assembly containing the API definition - /// - Assembly ApiAssembly { get; } - - /// - /// Returns a function that handles the middleware configuration for this module - /// - Action> ConfigureMiddleware { get; } - - /// - /// Returns the assembly containing the DDD domain types - /// - Assembly? DomainAssembly { get; } - - /// - /// Returns a function for using to register additional dependencies for this module - /// - Action? RegisterServices { get; } -} \ No newline at end of file diff --git a/src/Infrastructure.Web.Hosting.Common/ISubdomainModule.AspNet.cs b/src/Infrastructure.Web.Hosting.Common/ISubdomainModule.AspNet.cs new file mode 100644 index 00000000..8501a010 --- /dev/null +++ b/src/Infrastructure.Web.Hosting.Common/ISubdomainModule.AspNet.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Infrastructure.Web.Hosting.Common; + +partial interface ISubdomainModule +{ + /// + /// Returns a function that handles the request pipeline middleware configuration for this module + /// + Action> ConfigureMiddleware { get; } + + /// + /// Returns a function for registering additional dependencies into the dependency injection container + /// + Action? RegisterServices { get; } +} \ No newline at end of file diff --git a/src/Infrastructure.Web.Hosting.Common/ISubdomainModule.cs b/src/Infrastructure.Web.Hosting.Common/ISubdomainModule.cs new file mode 100644 index 00000000..e43f5bc2 --- /dev/null +++ b/src/Infrastructure.Web.Hosting.Common/ISubdomainModule.cs @@ -0,0 +1,25 @@ +using System.Reflection; + +namespace Infrastructure.Web.Hosting.Common; + +/// +/// Defines a individually deployable subdomain module +/// +// ReSharper disable once PartialTypeWithSinglePart +public partial interface ISubdomainModule +{ + /// + /// Returns the assembly containing the DDD domain types + /// + Assembly? DomainAssembly { get; } + + /// + /// Returns the naming prefix for each aggregate and each entity in the + /// + Dictionary EntityPrefixes { get; } + + /// + /// Returns the assembly containing the infrastructure and API definitions + /// + Assembly InfrastructureAssembly { get; } +} \ No newline at end of file diff --git a/src/Infrastructure.Web.Hosting.Common/SubDomainModules.cs b/src/Infrastructure.Web.Hosting.Common/SubdomainModules.cs similarity index 62% rename from src/Infrastructure.Web.Hosting.Common/SubDomainModules.cs rename to src/Infrastructure.Web.Hosting.Common/SubdomainModules.cs index 1d74114e..72c48365 100644 --- a/src/Infrastructure.Web.Hosting.Common/SubDomainModules.cs +++ b/src/Infrastructure.Web.Hosting.Common/SubdomainModules.cs @@ -9,20 +9,20 @@ namespace Infrastructure.Web.Hosting.Common; /// /// Modules used for registering subdomains /// -public class SubDomainModules +public class SubdomainModules { - private readonly Dictionary _aggregatePrefixes = new(); + private readonly Dictionary _entityPrefixes = new(); private readonly List _apiAssemblies = new(); - private readonly List _domainAssemblies = new(); + private readonly List _subdomainAssemblies = new(); private readonly List>> _minimalApiRegistrationFunctions = new(); private readonly List> _serviceCollectionFunctions = new(); - public IDictionary AggregatePrefixes => _aggregatePrefixes; + public IDictionary EntityPrefixes => _entityPrefixes; public IReadOnlyList ApiAssemblies => _apiAssemblies; - public IReadOnlyList DomainAssemblies => _domainAssemblies; + public IReadOnlyList SubdomainAssemblies => _subdomainAssemblies; /// /// Configure middleware in the pipeline @@ -32,21 +32,24 @@ public void ConfigureMiddleware(WebApplication app, List _minimalApiRegistrationFunctions.ForEach(func => func(app, middlewares)); } - public void Register(ISubDomainModule module) + /// + /// Registers all the information from the + /// + public void Register(ISubdomainModule module) { ArgumentNullException.ThrowIfNull(module); - ArgumentNullException.ThrowIfNull(module.ApiAssembly, nameof(module.ApiAssembly)); - ArgumentNullException.ThrowIfNull(module.ApiAssembly, nameof(module.AggregatePrefixes)); + ArgumentNullException.ThrowIfNull(module.InfrastructureAssembly, nameof(module.InfrastructureAssembly)); + ArgumentNullException.ThrowIfNull(module.InfrastructureAssembly, nameof(module.EntityPrefixes)); ArgumentNullException.ThrowIfNull(module.ConfigureMiddleware, nameof(module.ConfigureMiddleware)); - _apiAssemblies.Add(module.ApiAssembly); + _apiAssemblies.Add(module.InfrastructureAssembly); if (module.DomainAssembly.Exists()) { - _domainAssemblies.Add(module.DomainAssembly); + _subdomainAssemblies.Add(module.DomainAssembly); } - _aggregatePrefixes.Merge(module.AggregatePrefixes); + _entityPrefixes.Merge(module.EntityPrefixes); _minimalApiRegistrationFunctions.Add(module.ConfigureMiddleware); if (module.RegisterServices is not null) { @@ -54,6 +57,9 @@ public void Register(ISubDomainModule module) } } + /// + /// Registers all the services with the dependency injection container + /// public void RegisterServices(ConfigurationManager configuration, IServiceCollection serviceCollection) { _serviceCollectionFunctions.ForEach(func => func(configuration, serviceCollection)); diff --git a/src/OrganizationsInfrastructure/OrganizationsModule.cs b/src/OrganizationsInfrastructure/OrganizationsModule.cs index 56d662d8..f548b555 100644 --- a/src/OrganizationsInfrastructure/OrganizationsModule.cs +++ b/src/OrganizationsInfrastructure/OrganizationsModule.cs @@ -24,13 +24,13 @@ namespace OrganizationsInfrastructure; -public class OrganizationsModule : ISubDomainModule +public class OrganizationsModule : ISubdomainModule { - public Assembly ApiAssembly => typeof(OrganizationsModule).Assembly; + public Assembly InfrastructureAssembly => typeof(OrganizationsModule).Assembly; public Assembly DomainAssembly => typeof(OrganizationRoot).Assembly; - public Dictionary AggregatePrefixes => new() + public Dictionary EntityPrefixes => new() { { typeof(OrganizationRoot), "org" } }; diff --git a/src/SaaStack.sln.DotSettings b/src/SaaStack.sln.DotSettings index fe01bcd7..05487ebc 100644 --- a/src/SaaStack.sln.DotSettings +++ b/src/SaaStack.sln.DotSettings @@ -1082,6 +1082,7 @@ public void When$condition$_Then$outcome$() True True True + True True True True diff --git a/src/TestingStubApiHost/HostedModules.cs b/src/TestingStubApiHost/HostedModules.cs index 6ec0c594..891a967b 100644 --- a/src/TestingStubApiHost/HostedModules.cs +++ b/src/TestingStubApiHost/HostedModules.cs @@ -4,9 +4,9 @@ namespace TestingStubApiHost; public static class HostedModules { - public static SubDomainModules Get() + public static SubdomainModules Get() { - var modules = new SubDomainModules(); + var modules = new SubdomainModules(); #if TESTINGONLY modules.Register(new StubApiModule()); #endif diff --git a/src/TestingStubApiHost/StubApiModule.cs b/src/TestingStubApiHost/StubApiModule.cs index 0e1729c7..82e72f92 100644 --- a/src/TestingStubApiHost/StubApiModule.cs +++ b/src/TestingStubApiHost/StubApiModule.cs @@ -5,13 +5,13 @@ namespace TestingStubApiHost; -public class StubApiModule : ISubDomainModule +public class StubApiModule : ISubdomainModule { - public Assembly ApiAssembly => typeof(StubHelloApi).Assembly; + public Assembly InfrastructureAssembly => typeof(StubHelloApi).Assembly; public Assembly DomainAssembly => typeof(StubHelloApi).Assembly; - public Dictionary AggregatePrefixes => new(); + public Dictionary EntityPrefixes => new(); public Action> ConfigureMiddleware { diff --git a/src/Tools.Analyzers.Common/AnalyzerConstants.cs b/src/Tools.Analyzers.Common/AnalyzerConstants.cs index 3c760ba6..361a1030 100644 --- a/src/Tools.Analyzers.Common/AnalyzerConstants.cs +++ b/src/Tools.Analyzers.Common/AnalyzerConstants.cs @@ -29,5 +29,6 @@ public static class Categories public const string Ddd = "SaaStackDDD"; public const string Documentation = "SaaStackDocumentation"; public const string WebApi = "SaaStackWebApi"; + public const string Host = "SaaStackHosts"; } } \ No newline at end of file diff --git a/src/Tools.Analyzers.Common/Extensions/SymbolExtensions.cs b/src/Tools.Analyzers.Common/Extensions/SymbolExtensions.cs index d39f3775..2d20b657 100644 --- a/src/Tools.Analyzers.Common/Extensions/SymbolExtensions.cs +++ b/src/Tools.Analyzers.Common/Extensions/SymbolExtensions.cs @@ -6,6 +6,19 @@ namespace Tools.Analyzers.Common.Extensions; public static class SymbolExtensions { + public static ExpressionSyntax? GetGetterExpression(this ISymbol method) + { + var syntaxReference = method.DeclaringSyntaxReferences.FirstOrDefault(); + + var syntax = syntaxReference?.GetSyntax(); + if (syntax is ArrowExpressionClauseSyntax arrowExpressionClauseSyntax) + { + return arrowExpressionClauseSyntax.Expression; + } + + return null; + } + public static string GetMethodBody(this ISymbol method) { var syntaxReference = method.DeclaringSyntaxReferences.FirstOrDefault(); diff --git a/src/Tools.Analyzers.NonPlatform.UnitTests/SubdomainModuleAnalyzerSpec.cs b/src/Tools.Analyzers.NonPlatform.UnitTests/SubdomainModuleAnalyzerSpec.cs new file mode 100644 index 00000000..1e935031 --- /dev/null +++ b/src/Tools.Analyzers.NonPlatform.UnitTests/SubdomainModuleAnalyzerSpec.cs @@ -0,0 +1,245 @@ +extern alias NonPlatformAnalyzers; +using UsedImplicitly = NonPlatformAnalyzers::JetBrains.Annotations.UsedImplicitlyAttribute; +using Xunit; +using SubdomainModuleAnalyzer = NonPlatformAnalyzers::Tools.Analyzers.NonPlatform.SubdomainModuleAnalyzer; + +namespace Tools.Analyzers.NonPlatform.UnitTests; + +[UsedImplicitly] +public class SubdomainModuleAnalyzerSpec +{ + [UsedImplicitly] + public class GivenAnySubdomainModule + { + [Trait("Category", "Unit")] + public class GivenAnyRule + { + [Fact] + public async Task WhenDomainAssembly_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using System.Reflection; +using Infrastructure.Web.Hosting.Common; +namespace ANamespace; +public class AClass : ISubdomainModule +{ + public Assembly InfrastructureAssembly => null!; + + public Assembly? DomainAssembly => null; + + public Dictionary EntityPrefixes => new Dictionary(); +}"; + + await Verify.NoDiagnosticExists(input); + } + } + + [Trait("Category", "Unit")] + public class GivenRule010 + { + [Fact] + public async Task WhenNoAggregatesInDomainAssembly_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using System.Reflection; +using Infrastructure.Web.Hosting.Common; +namespace ANamespace; +public class AClass : ISubdomainModule +{ + public Assembly InfrastructureAssembly => null!; + + public Assembly? DomainAssembly => typeof(AnAggregateRoot).Assembly; + + public Dictionary EntityPrefixes => new Dictionary(); +} + +public class AnAggregateRoot +{ +} +"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAggregateRootNotRegistered_ThenAlerts() + { + const string input = @" +using System; +using System.Collections.Generic; +using System.Reflection; +using Infrastructure.Web.Hosting.Common; +using Domain.Interfaces.Entities; +namespace ANamespace; +public class AClass : ISubdomainModule +{ + public Assembly InfrastructureAssembly => null!; + + public Assembly? DomainAssembly => typeof(AnAggregateRoot).Assembly; + + public Dictionary EntityPrefixes => new Dictionary(); +} + +public class AnAggregateRoot : IAggregateRoot +{ +} +"; + + await Verify.DiagnosticExists( + SubdomainModuleAnalyzer.Rule010, input, 14, 37, "EntityPrefixes", "AnAggregateRoot"); + } + + [Fact] + public async Task WhenAggregateRootIsRegistered_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using System.Reflection; +using Infrastructure.Web.Hosting.Common; +using Domain.Interfaces.Entities; +namespace ANamespace; +public class AClass : ISubdomainModule +{ + public Assembly InfrastructureAssembly => null!; + + public Assembly? DomainAssembly => typeof(AnAggregateRoot).Assembly; + + public Dictionary EntityPrefixes => new Dictionary + { + { typeof(AnAggregateRoot), ""aprefix"" } + }; +} + +public class AnAggregateRoot : IAggregateRoot +{ +} +"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenNoEntitiesInDomainAssembly_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using System.Reflection; +using Infrastructure.Web.Hosting.Common; +namespace ANamespace; +public class AClass : ISubdomainModule +{ + public Assembly InfrastructureAssembly => null!; + + public Assembly? DomainAssembly => typeof(AnEntity).Assembly; + + public Dictionary EntityPrefixes => new Dictionary(); +} + +public class AnEntity +{ +} +"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenEntityNotRegistered_ThenAlerts() + { + const string input = @" +using System; +using System.Collections.Generic; +using System.Reflection; +using Infrastructure.Web.Hosting.Common; +using Domain.Interfaces.Entities; +namespace ANamespace; +public class AClass : ISubdomainModule +{ + public Assembly InfrastructureAssembly => null!; + + public Assembly? DomainAssembly => typeof(AnEntity).Assembly; + + public Dictionary EntityPrefixes => new Dictionary(); +} + +public class AnEntity : IEntity +{ +} +"; + + await Verify.DiagnosticExists( + SubdomainModuleAnalyzer.Rule010, input, 14, 37, "EntityPrefixes", "AnEntity"); + } + + [Fact] + public async Task WhenEntityIsRegistered_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using System.Reflection; +using Infrastructure.Web.Hosting.Common; +using Domain.Interfaces.Entities; +namespace ANamespace; +public class AClass : ISubdomainModule +{ + public Assembly InfrastructureAssembly => null!; + + public Assembly? DomainAssembly => typeof(AnEntity).Assembly; + + public Dictionary EntityPrefixes => new Dictionary + { + { typeof(AnEntity), ""aprefix"" } + }; +} + +public class AnEntity : IEntity +{ +} +"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAggregateAndEntityIsRegistered_ThenNoAlert() + { + const string input = @" +using System; +using System.Collections.Generic; +using System.Reflection; +using Infrastructure.Web.Hosting.Common; +using Domain.Interfaces.Entities; +namespace ANamespace; +public class AClass : ISubdomainModule +{ + public Assembly InfrastructureAssembly => null!; + + public Assembly? DomainAssembly => typeof(AnEntity).Assembly; + + public Dictionary EntityPrefixes => new Dictionary + { + { typeof(AnEntity), ""aprefix"" }, + { typeof(AnAggregateRoot), ""aprefix"" } + }; +} + +public class AnEntity : IEntity +{ +} +public class AnAggregateRoot : IAggregateRoot +{ +} +"; + + await Verify.NoDiagnosticExists(input); + } + } + } +} \ No newline at end of file diff --git a/src/Tools.Analyzers.NonPlatform/AnalyzerReleases.Shipped.md b/src/Tools.Analyzers.NonPlatform/AnalyzerReleases.Shipped.md index 74778e4c..20b189ae 100644 --- a/src/Tools.Analyzers.NonPlatform/AnalyzerReleases.Shipped.md +++ b/src/Tools.Analyzers.NonPlatform/AnalyzerReleases.Shipped.md @@ -58,4 +58,5 @@ SAASAPP022 | SaaStackApplication | Error | ReadModels must have a parameterless constructor SAASAPP023 | SaaStackApplication | Error | Properties must have public getters and setters SAASAPP024 | SaaStackApplication | Warning | Properties must be Optional{T} not nullable - SAASAPP025 | SaaStackApplication | Error | Properties must of correct type \ No newline at end of file + SAASAPP025 | SaaStackApplication | Error | Properties must of correct type + SAASHST010 | SaaStackHosts | Error | Aggregate root or Entity should register an identity prefix \ No newline at end of file diff --git a/src/Tools.Analyzers.NonPlatform/ApiLayerAnalyzer.cs b/src/Tools.Analyzers.NonPlatform/ApiLayerAnalyzer.cs index 1b1e7442..0ff241cf 100644 --- a/src/Tools.Analyzers.NonPlatform/ApiLayerAnalyzer.cs +++ b/src/Tools.Analyzers.NonPlatform/ApiLayerAnalyzer.cs @@ -10,12 +10,10 @@ using Tools.Analyzers.Common.Extensions; using Tools.Analyzers.NonPlatform.Extensions; -// ReSharper disable InvalidXmlDocComment - namespace Tools.Analyzers.NonPlatform; /// -/// An analyzer to the correct implementation of WebAPI classes, and their requests and responses. +/// An analyzer to correct the implementation of WebAPI classes, and their requests and responses. /// WebApiServices: /// SAASWEB10: Warning: Methods that are public, should return a or just any T, where T is /// either: diff --git a/src/Tools.Analyzers.NonPlatform/ApplicationLayerAnalyzer.cs b/src/Tools.Analyzers.NonPlatform/ApplicationLayerAnalyzer.cs index aeb48992..d7d09fc9 100644 --- a/src/Tools.Analyzers.NonPlatform/ApplicationLayerAnalyzer.cs +++ b/src/Tools.Analyzers.NonPlatform/ApplicationLayerAnalyzer.cs @@ -14,7 +14,7 @@ namespace Tools.Analyzers.NonPlatform; /// -/// An analyzer to the correct implementation of application DTOs and ReadModels: +/// An analyzer to correct the implementation of application DTOs and ReadModels: /// Application Resources: /// SAASAPP010: Error: Resources must be public /// SAASAPP011: Error: Resources must have a parameterless constructor diff --git a/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignAnalyzer.cs b/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignAnalyzer.cs index d840a7a5..1bf2795d 100644 --- a/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignAnalyzer.cs +++ b/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignAnalyzer.cs @@ -16,7 +16,7 @@ namespace Tools.Analyzers.NonPlatform; /// -/// An analyzer to the correct implementation of root aggregates, entities, value objects and domain events: +/// An analyzer to correct the implementation of root aggregates, entities, value objects and domain events: /// Aggregate Roots: /// SAASDDD010: Error: Aggregate roots must have at least one Create() class factory method /// SAASDDD011: Error: Create() class factory methods must return correct types diff --git a/src/Tools.Analyzers.NonPlatform/Resources.Designer.cs b/src/Tools.Analyzers.NonPlatform/Resources.Designer.cs index 064cf5a3..c7bdab53 100644 --- a/src/Tools.Analyzers.NonPlatform/Resources.Designer.cs +++ b/src/Tools.Analyzers.NonPlatform/Resources.Designer.cs @@ -815,6 +815,33 @@ internal static string SAASDDD049Title { } } + /// + /// Looks up a localized string similar to Aggregate or Entity should be registered with an identity prefix. + /// + internal static string SAASHST010Description { + get { + return ResourceManager.GetString("SAASHST010Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} is missing an entry for a prefix for '{1}'. + /// + internal static string SAASHST010MessageFormat { + get { + return ResourceManager.GetString("SAASHST010MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Missing prefix registration. + /// + internal static string SAASHST010Title { + get { + return ResourceManager.GetString("SAASHST010Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to This method should return a 'Result<T>' type.. /// @@ -1086,7 +1113,7 @@ internal static string SAASWEB019Title { } /// - /// Looks up a localized string similar to The request type should be suffixed with the word 'Request'.. + /// Looks up a localized string similar to The request type should end with the word 'Request'.. /// internal static string SAASWEB031Description { get { @@ -1095,7 +1122,7 @@ internal static string SAASWEB031Description { } /// - /// Looks up a localized string similar to Request '{0}' should be suffixed with the word 'Request'. + /// Looks up a localized string similar to Request '{0}' should end with the word 'Request'. /// internal static string SAASWEB031MessageFormat { get { @@ -1167,7 +1194,7 @@ internal static string SAASWEB033Title { } /// - /// Looks up a localized string similar to The request type should be suffixed with the word 'Response'.. + /// Looks up a localized string similar to The request type should end with the word 'Response'.. /// internal static string SAASWEB041Description { get { @@ -1176,7 +1203,7 @@ internal static string SAASWEB041Description { } /// - /// Looks up a localized string similar to Request '{0}' should be suffixed with the word 'Response'. + /// Looks up a localized string similar to Request '{0}' should be end with the word 'Response'. /// internal static string SAASWEB041MessageFormat { get { diff --git a/src/Tools.Analyzers.NonPlatform/Resources.resx b/src/Tools.Analyzers.NonPlatform/Resources.resx index b9917162..c75cb6f1 100644 --- a/src/Tools.Analyzers.NonPlatform/Resources.resx +++ b/src/Tools.Analyzers.NonPlatform/Resources.resx @@ -225,10 +225,10 @@ Missing '[AuthorizeAttribute]' - The request type should be suffixed with the word 'Request'. + The request type should end with the word 'Request'. - Request '{0}' should be suffixed with the word 'Request' + Request '{0}' should end with the word 'Request' Naming is wrong @@ -252,10 +252,10 @@ Missing '[RouteAttribute]' on request - The request type should be suffixed with the word 'Response'. + The request type should end with the word 'Response'. - Request '{0}' should be suffixed with the word 'Response' + Request '{0}' should be end with the word 'Response' Naming is wrong @@ -389,6 +389,16 @@ Wrong return type + + Aggregate or Entity should be registered with an identity prefix + + + {0} is missing an entry for a prefix for '{1}' + + + Missing prefix registration + + Add missing 'Rehydrate()' method diff --git a/src/Tools.Analyzers.NonPlatform/SubdomainModuleAnalyzer.cs b/src/Tools.Analyzers.NonPlatform/SubdomainModuleAnalyzer.cs new file mode 100644 index 00000000..3f902629 --- /dev/null +++ b/src/Tools.Analyzers.NonPlatform/SubdomainModuleAnalyzer.cs @@ -0,0 +1,187 @@ +using System.Collections.Immutable; +using Common.Extensions; +using Domain.Interfaces.Entities; +using Infrastructure.Web.Hosting.Common; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Tools.Analyzers.Common; +using Tools.Analyzers.Common.Extensions; +using Tools.Analyzers.NonPlatform.Extensions; + +namespace Tools.Analyzers.NonPlatform; + +/// +/// An analyzer to correct the implementation of WebAPI classes, and their requests and responses. +/// SAASHST10: Error: Aggregate root or Entity should register an identity prefix +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class SubdomainModuleAnalyzer : DiagnosticAnalyzer +{ + internal static readonly DiagnosticDescriptor Rule010 = "SAASHST10".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.Host, nameof(Resources.SAASHST010Title), nameof(Resources.SAASHST010Description), + nameof(Resources.SAASHST010MessageFormat)); + + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(Rule010); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeSubdomainModule, SyntaxKind.ClassDeclaration); + } + + private static void AnalyzeSubdomainModule(SyntaxNodeAnalysisContext context) + { + var methodSyntax = context.Node; + if (methodSyntax is not ClassDeclarationSyntax classDeclarationSyntax) + { + return; + } + + if (context.IsExcludedInNamespace(classDeclarationSyntax, AnalyzerConstants.PlatformNamespaces)) + { + return; + } + + if (classDeclarationSyntax.IsNotType(context)) + { + return; + } + + var domainAssemblyProperty = classDeclarationSyntax.Members + .OfType() + .FirstOrDefault(method => method.Identifier.Text == nameof(ISubdomainModule.DomainAssembly)); + if (domainAssemblyProperty is null) + { + return; + } + + var assemblySymbol = domainAssemblyProperty.GetAssemblyFromProperty(context); + if (assemblySymbol is null) + { + return; + } + + var allEntities = assemblySymbol.GetAllEntityTypes(context); + if (allEntities.HasNone()) + { + return; + } + + var entityPrefixesProperty = classDeclarationSyntax.Members + .OfType() + .FirstOrDefault(method => method.Identifier.Text == nameof(ISubdomainModule.EntityPrefixes)); + if (entityPrefixesProperty is null) + { + return; + } + + var allRegisteredPrefixes = entityPrefixesProperty.GetPrefixesFromProperty(context); + if (allRegisteredPrefixes.HasNone()) + { + foreach (var entity in allEntities) + { + context.ReportDiagnostic(Rule010, entityPrefixesProperty, entity.Name); + } + + return; + } + + foreach (var entity in allEntities) + { + if (!allRegisteredPrefixes.Contains(entity)) + { + context.ReportDiagnostic(Rule010, entityPrefixesProperty, entity.Name); + } + } + } +} + +internal static class SubdomainModuleAnalyzerExtensions +{ + public static List GetAllEntityTypes(this IAssemblySymbol assembly, + SyntaxNodeAnalysisContext context) + { + var aggregate = context.Compilation.GetTypeByMetadataName(typeof(IAggregateRoot).FullName!)!; + var entity = context.Compilation.GetTypeByMetadataName(typeof(IEntity).FullName!)!; + + return assembly.GlobalNamespace.GetMembers() + .SelectMany(ns => ns.GetMembers()) + .OfType() + .Where(type => type.AllInterfaces.Contains(aggregate) || type.AllInterfaces.Contains(entity)) + .ToList(); + } + + public static IAssemblySymbol? GetAssemblyFromProperty(this PropertyDeclarationSyntax property, + SyntaxNodeAnalysisContext context) + { + var symbol = context.SemanticModel.GetDeclaredSymbol(property); + if (symbol is null) + { + return null; + } + + var getter = symbol.GetMethod; + if (getter is null) + { + return null; + } + + var body = getter.GetGetterExpression(); + if (body is null) + { + return null; + } + + var someType = body.DescendantNodes() + .OfType() + .Select(expr => expr.Type) + .FirstOrDefault(); + if (someType is null) + { + return null; + } + + return context.SemanticModel.GetSymbolInfo(someType) + .Symbol?.ContainingAssembly; + } + + public static List GetPrefixesFromProperty(this PropertyDeclarationSyntax property, + SyntaxNodeAnalysisContext context) + { + var symbol = context.SemanticModel.GetDeclaredSymbol(property); + if (symbol is null) + { + return new List(); + } + + var getter = symbol.GetMethod; + if (getter is null) + { + return new List(); + } + + var body = getter.GetGetterExpression(); + if (body is null) + { + return new List(); + } + + var someTypes = body.DescendantNodes() + .OfType() + .Select(expr => expr.Type) + .ToList(); + if (someTypes.HasNone()) + { + return new List(); + } + + return someTypes + .Select(someType => context.SemanticModel.GetSymbolInfo(someType).Symbol) + .OfType() + .ToList(); + } +} \ No newline at end of file diff --git a/src/Tools.Analyzers.NonPlatform/Tools.Analyzers.NonPlatform.csproj b/src/Tools.Analyzers.NonPlatform/Tools.Analyzers.NonPlatform.csproj index 7a9fc9af..28186236 100644 --- a/src/Tools.Analyzers.NonPlatform/Tools.Analyzers.NonPlatform.csproj +++ b/src/Tools.Analyzers.NonPlatform/Tools.Analyzers.NonPlatform.csproj @@ -290,6 +290,9 @@ Reference\Domain.Common\ValueObjects\EventMetadataExtensions.cs + + Reference\Infrastructure.Web.Hosting.Common\ISubdomainModule.cs + diff --git a/src/Tools.Templates/HostProject/HostedModules.cs b/src/Tools.Templates/HostProject/HostedModules.cs index 37bfe3e3..ecf9bf33 100644 --- a/src/Tools.Templates/HostProject/HostedModules.cs +++ b/src/Tools.Templates/HostProject/HostedModules.cs @@ -6,7 +6,7 @@ public static class HostedModules { public static SubDomainModules Get() { - // EXTEND: Add the sub domain of each API, to host in this project. + // EXTEND: Add the subdomain of each API, to host in this project. // NOTE: The order of these registrations will matter for some dependencies var modules = new SubDomainModules(); diff --git a/src/Tools.Templates/InfrastructureProject/ProjectNameModule.cs b/src/Tools.Templates/InfrastructureProject/ProjectNameModule.cs index 8f1a78f4..1bc6b490 100644 --- a/src/Tools.Templates/InfrastructureProject/ProjectNameModule.cs +++ b/src/Tools.Templates/InfrastructureProject/ProjectNameModule.cs @@ -16,7 +16,7 @@ public class {SubDomainName}sModule : ISubDomainModule { public Assembly ApiAssembly => typeof({SubDomainName}sApi).Assembly; - public Assembly DomainAssembly => typeof({SubDomainName}Root).Assembly; + public Assembly? DomainAssembly => typeof({SubDomainName}Root).Assembly; public Dictionary AggregatePrefixes => new() { diff --git a/src/WebsiteHost/BackEndForFrontEndModule.cs b/src/WebsiteHost/BackEndForFrontEndModule.cs index e3dfc79c..3d13a7e7 100644 --- a/src/WebsiteHost/BackEndForFrontEndModule.cs +++ b/src/WebsiteHost/BackEndForFrontEndModule.cs @@ -12,13 +12,13 @@ namespace WebsiteHost; -public class BackEndForFrontEndModule : ISubDomainModule +public class BackEndForFrontEndModule : ISubdomainModule { - public Assembly ApiAssembly => typeof(RecordingApi).Assembly; + public Assembly InfrastructureAssembly => typeof(RecordingApi).Assembly; public Assembly? DomainAssembly => null; - public Dictionary AggregatePrefixes => new(); + public Dictionary EntityPrefixes => new(); public Action> ConfigureMiddleware { diff --git a/src/WebsiteHost/HostedModules.cs b/src/WebsiteHost/HostedModules.cs index 540cb436..50baae1c 100644 --- a/src/WebsiteHost/HostedModules.cs +++ b/src/WebsiteHost/HostedModules.cs @@ -4,9 +4,9 @@ namespace WebsiteHost; public static class HostedModules { - public static SubDomainModules Get() + public static SubdomainModules Get() { - var modules = new SubDomainModules(); + var modules = new SubdomainModules(); modules.Register(new BackEndForFrontEndModule()); return modules;