From 24b2e16ce203beac04019bdde8b26052035d996d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 4 Nov 2020 16:19:05 +0300 Subject: [PATCH 1/2] Documented Value Objects --- docs/en/Domain-Driven-Design.md | 2 +- docs/en/Value-Objects.md | 76 ++++++++++++++++++- docs/en/docs-nav.json | 3 +- .../Volo/Abp/Domain/Values/Address.cs | 36 +++++++++ .../Abp/Domain/Values/ValueObject_Tests.cs | 30 ++++++++ 5 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Values/Address.cs create mode 100644 framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Values/ValueObject_Tests.cs diff --git a/docs/en/Domain-Driven-Design.md b/docs/en/Domain-Driven-Design.md index 9e0343d562..fe134a28e7 100644 --- a/docs/en/Domain-Driven-Design.md +++ b/docs/en/Domain-Driven-Design.md @@ -27,9 +27,9 @@ See the following documents to learn what ABP Framework provides to you to imple * **Domain Layer** * [Entities & Aggregate Roots](Entities.md) - * Value Objects * [Repositories](Repositories.md) * [Domain Services](Domain-Services.md) + * [Value Objects](Value-Objects.md) * Specifications * **Application Layer** * [Application Services](Application-Services.md) diff --git a/docs/en/Value-Objects.md b/docs/en/Value-Objects.md index 0c6c8d4424..f61e32245b 100644 --- a/docs/en/Value-Objects.md +++ b/docs/en/Value-Objects.md @@ -1,3 +1,75 @@ -## Value Objects +# Value Objects -TODO \ No newline at end of file +> An object that represents a descriptive aspect of the domain with no conceptual identity is called a VALUE OBJECT. +> +> (Eric Evans) + +Two [Entities](Entities.md) with the same properties but with different `Id`s are considered as different entities. However, Value Objects have no `Id`s and they are considered as equals if they have the same property values. + +## The ValueObject Class + +`ValueObject` is an abstract class that can be inherited to create a Value Object class. + +**Example: An Address class** + +````csharp +public class Address : ValueObject +{ + public Guid CityId { get; private set; } + + public string Street { get; private set; } + + public int Number { get; private set; } + + private Address() + { + + } + + public Address( + Guid cityId, + string street, + int number) + { + CityId = cityId; + Street = street; + Number = number; + } + + protected override IEnumerable GetAtomicValues() + { + yield return Street; + yield return CityId; + yield return Number; + } +} +```` + +* A Value Object class must implement the `GetAtomicValues()` method to return the primitive values. + +### ValueEquals + +`ValueObject.ValueEquals(...)` method is used to check if two Value Objects are equals. + +**Example: Check if two addresses are equals** + +````csharp +Address address1 = ... +Address address2 = ... + +if (address1.ValueEquals(address2)) //Check equality +{ + ... +} +```` + +## Best Practices + +Here are some best practices when using Value Objects: + +- Design a value object as **immutable** (like the Address above) if there is not a good reason for designing it as mutable. +- The properties that make up a Value Object should form a conceptual whole. For example, CityId, Street and Number shouldn't be separate properties of a Person entity. This also makes the Person entity simpler. + +## See Also + +* [Entities](Entities.md) \ No newline at end of file diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 29d658aee4..a3dcc17dcf 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -340,7 +340,8 @@ "path": "Entities.md" }, { - "text": "Value Objects" + "text": "Value Objects", + "path": "Value-Objects.md" }, { "text": "Repositories", diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Values/Address.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Values/Address.cs new file mode 100644 index 0000000000..0fdc29280f --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Values/Address.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.Domain.Values +{ + public class Address : ValueObject + { + public Guid CityId { get; } + + public string Street { get; } + + public int Number { get; } + + private Address() + { + } + + public Address( + Guid cityId, + string street, + int number) + { + CityId = cityId; + Street = street; + Number = number; + } + + //Requires to implement this method to return properties. + protected override IEnumerable GetAtomicValues() + { + yield return Street; + yield return CityId; + yield return Number; + } + } +} diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Values/ValueObject_Tests.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Values/ValueObject_Tests.cs new file mode 100644 index 0000000000..57cc2a6c60 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Values/ValueObject_Tests.cs @@ -0,0 +1,30 @@ +using System; +using Shouldly; +using Xunit; + +namespace Volo.Abp.Domain.Values +{ + public class ValueObject_Tests + { + [Fact] + public void ValueObjects_With_Same_Properties_Should_Be_Equals() + { + var cityId = Guid.NewGuid(); + var address1 = new Address(cityId, "Baris Manco", 42); + var address2 = new Address(cityId, "Baris Manco", 42); + + address1.ValueEquals(address2).ShouldBeTrue(); + } + + [Fact] + public void ValueObjects_With_Different_Properties_Should_Not_Be_Equals() + { + var cityId = Guid.NewGuid(); + + var address1 = new Address(cityId, "Baris Manco", 42); + var address2 = new Address(cityId, "Baris Manco", 42); + + address1.ValueEquals(address2).ShouldBeFalse(); + } + } +} From 91aa018aa1bd21268c0d06e4823ceb35d1a5015d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 4 Nov 2020 17:53:10 +0300 Subject: [PATCH 2/2] Documented the Specifications --- docs/en/Domain-Driven-Design.md | 2 +- docs/en/Specifications.md | 250 +++++++++++++++++++++++++++++++- 2 files changed, 250 insertions(+), 2 deletions(-) diff --git a/docs/en/Domain-Driven-Design.md b/docs/en/Domain-Driven-Design.md index fe134a28e7..f3d2699cab 100644 --- a/docs/en/Domain-Driven-Design.md +++ b/docs/en/Domain-Driven-Design.md @@ -30,7 +30,7 @@ See the following documents to learn what ABP Framework provides to you to imple * [Repositories](Repositories.md) * [Domain Services](Domain-Services.md) * [Value Objects](Value-Objects.md) - * Specifications + * [Specifications](Specifications.md) * **Application Layer** * [Application Services](Application-Services.md) * [Data Transfer Objects (DTOs)](Data-Transfer-Objects.md) diff --git a/docs/en/Specifications.md b/docs/en/Specifications.md index ea4f140e95..80c10328ae 100644 --- a/docs/en/Specifications.md +++ b/docs/en/Specifications.md @@ -1,3 +1,251 @@ # Specifications -TODO! \ No newline at end of file +Specification Pattern is used to define **named, reusable, combinable and testable filters** for entities and other business objects. + +## Installation + +If you haven't installed yet, install the [Volo.Abp.Specifications](https://abp.io/package-detail/Volo.Abp.Specifications) package to your project. You can use the [ABP CLI](CLI.md) *add-package* command in a command line terminal when the current folder is the root folder of your project (`.csproj`): + +````bash +abp add-package Volo.Abp.Specifications +```` + +## Defining the Specifications + +Assume that you've an Customer entity as defined below: + +````csharp +using System; +using Volo.Abp.Domain.Entities; + +namespace MyProject +{ + public class Customer : AggregateRoot + { + public string Name { get; set; } + + public byte Age { get; set; } + + public long Balance { get; set; } + + public string Location { get; set; } + } +} +```` + +You can create a new Specification class derived from the `Specification`. + +**Example: A specification to select the customers with 18+ age:** + +````csharp +using System; +using System.Linq.Expressions; +using Volo.Abp.Specifications; + +namespace MyProject +{ + public class Age18PlusCustomerSpecification : Specification + { + public override Expression> ToExpression() + { + return c => c.Age >= 18; + } + } +} +```` + +You simply define a lambda [Expression](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions) to define a specification. + +> Instead, you can directly implement the `ISpecification` interface, but the `Specification` base class much simplifies it. + +## Using the Specifications + +There are two common use cases of the specifications. + +### IsSatisfiedBy + +`IsSatisfiedBy` method can be used to check if a single object satisfies the specification. + +**Example: Throw exception if the customer doesn't satisfy the age specification** + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace MyProject +{ + public class CustomerService : ITransientDependency + { + public async Task BuyAlcohol(Customer customer) + { + if (!new Age18PlusCustomerSpecification().IsSatisfiedBy(customer)) + { + throw new Exception( + "This customer doesn't satisfy the Age specification!" + ); + } + + //TODO... + } + } +} +```` + +### ToExpression & Repositories + +`ToExpression()` method can be used to use the specification as Expression. In this way, you can use a specification to **filter entities while querying from the database**. + +````csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Services; + +namespace MyProject +{ + public class CustomerManager : DomainService, ITransientDependency + { + private readonly IRepository _customerRepository; + + public CustomerManager(IRepository customerRepository) + { + _customerRepository = customerRepository; + } + + public async Task> GetCustomersCanBuyAlcohol() + { + var query = _customerRepository.Where( + new Age18PlusCustomerSpecification().ToExpression() + ); + + return await AsyncExecuter.ToListAsync(query); + } + } +} +```` + +> Specifications are correctly translated to SQL/Database queries and executed efficiently in the DBMS side. While it is not related to the Specifications, see the [Repositories](Repositories.md) document if you want to know more about the `AsyncExecuter`. + +Actually, using the `ToExpression()` method is not necessary since the specifications are automatically casted to Expressions. This would also work: + +````csharp +var query = _customerRepository.Where( + new Age18PlusCustomerSpecification() +); +```` + +## Composing the Specifications + +One powerful feature of the specifications is that they are composable with `And`, `Or`, `Not` and `AndNot` extension methods. + +Assume that you have another specification as defined below: + +```csharp +using System; +using System.Linq.Expressions; +using Volo.Abp.Specifications; + +namespace MyProject +{ + public class PremiumCustomerSpecification : Specification + { + public override Expression> ToExpression() + { + return (customer) => (customer.Balance >= 100000); + } + } +} +``` + +You can combine the `PremiumCustomerSpecification` with the `Age18PlusCustomerSpecification` to query the count of premium adult customers as shown below: + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Services; +using Volo.Abp.Specifications; + +namespace MyProject +{ + public class CustomerManager : DomainService, ITransientDependency + { + private readonly IRepository _customerRepository; + + public CustomerManager(IRepository customerRepository) + { + _customerRepository = customerRepository; + } + + public async Task GetAdultPremiumCustomerCountAsync() + { + return await _customerRepository.CountAsync( + new Age18PlusCustomerSpecification() + .And(new PremiumCustomerSpecification()).ToExpression() + ); + } + } +} +```` + +If you want to make this combination another reusable specification, you can create such a combination specification class deriving from the `AndSpecification`: + +````csharp +using Volo.Abp.Specifications; + +namespace MyProject +{ + public class AdultPremiumCustomerSpecification : AndSpecification + { + public AdultPremiumCustomerSpecification() + : base(new Age18PlusCustomerSpecification(), + new PremiumCustomerSpecification()) + { + } + } +} +```` + +Now, you can re-write the `GetAdultPremiumCustomerCountAsync` method as shown below: + +````csharp +public async Task GetAdultPremiumCustomerCountAsync() +{ + return await _customerRepository.CountAsync( + new AdultPremiumCustomerSpecification() + ); +} +```` + +> You see the power of the specifications with these samples. If you change the `PremiumCustomerSpecification` later, say change the balance from `100.000` to `200.000`, all the queries and combined specifications will be effected by the change. This is a good way to reduce code duplication! + +## Discussions + +While the specification pattern is older than C# lambda expressions, it's generally compared to expressions. Some developers may think it's not needed anymore and we can directly pass expressions to a repository or to a domain service as shown below: + +``` +var count = await _customerRepository.CountAsync(c => c.Balance > 100000 && c.Age => 18); +``` + +Since ABP's [Repository](Repositories.md) supports Expressions, this is a completely valid use. You don't have to define or use any specification in your application and you can go with expressions. + +So, what's the point of a specification? Why and when should we consider to use them? + +### When To Use? + +Some benefits of using specifications: + +- **Reusabe**: Imagine that you need the Premium Customer filter in many places in your code base. If you go with expressions and do not create a specification, what happens if you later change the "Premium Customer" definition? Say you want to change the minimum balance from $100,000 to $250,000 and add another condition to be a customer older than 3 years. If you'd used a specification, you just change a single class. If you repeated (copy/pasted) the same expression everywhere, you need to change all of them. +- **Composable**: You can combine multiple specifications to create new specifications. This is another type of reusability. +- **Named**: `PremiumCustomerSpecification` better explains the intent rather than a complex expression. So, if you have an expression that is meaningful in your business, consider using specifications. +- **Testable**: A specification is a separately (and easily) testable object. + +### When To Not Use? + +- **Non business expressions**: Do not use specifications for non business-related expressions and operations. +- **Reporting**: If you are just creating a report, do not create specifications, but directly use `IQueryable` & LINQ expressions. You can even use plain SQL, views or another tool for reporting. DDD does not necessarily care about reporting, so the way you query the underlying data store can be important from a performance perspective. \ No newline at end of file