From f88f169ea2212b33e93990cc2e804cba711b2880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Vittorelli?= Date: Sun, 26 Sep 2021 00:22:22 +0200 Subject: [PATCH 1/4] docs: Improve readability of Overview.md --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index d3fa4292..739f2a01 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,7 +6,7 @@ has_children: false --- # Overview -The [Specification pattern](https://deviq.com/design-patterns/specification-pattern) encapsulates query logic in its own class, which helps classes follow the [Single Responsibility Principle](https://deviq.com/principles/single-responsibility-principle) (SRP) and promotes reuse of common queries. Specifications can be independently unit tested and when combined with [Repository](https://deviq.com/design-patterns/repository-pattern) help keep the Repository from growing with too many additional custom query methods. Specification is commonly used on projects that leverage [Domain-Driven Design](https://deviq.com/domain-driven-design/ddd-overview). +The [Specification pattern](https://deviq.com/design-patterns/specification-pattern) encapsulates query logic in its own class, which helps classes follow the [Single Responsibility Principle](https://deviq.com/principles/single-responsibility-principle) (SRP) and promotes reuse of common queries. Specifications can be independently unit tested. When combined with the [Repository](https://deviq.com/design-patterns/repository-pattern) pattern, it can also help to keep it from growing with too many additional custom query methods. Specification is commonly used on projects that leverage [Domain-Driven Design](https://deviq.com/domain-driven-design/ddd-overview). Since version 5, this package also supports applying specifications directly to EF Core `DbContext` instances. From e657ac8711ce55beea977c7403f76de38caf7443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Vittorelli?= Date: Sun, 26 Sep 2021 00:30:10 +0200 Subject: [PATCH 2/4] docs: Builder extension method example --- .../create-specification-builder.md | 68 ++++++++++++++++++- docs/features/caching.md | 2 +- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/docs/extensions/create-specification-builder.md b/docs/extensions/create-specification-builder.md index 4685a15b..c1c5fc37 100644 --- a/docs/extensions/create-specification-builder.md +++ b/docs/extensions/create-specification-builder.md @@ -5,4 +5,70 @@ parent: Extensions nav_order: 2 --- -How to create your own specification builder + +# How to create your own specification builder +How to create your own specification builder + +## Example: Configure caching behaviour through specification builder extension method + +In order to achieve this: + +````csharp +public class CustomerByNameWithStores : Specification +{ + public CustomerByNameWithStores(string name) + { + Query.Where(x => x.Name == name) + .EnableCache(nameof(CustomerByNameWithStoresSpec), name) + // Can only be called after .EnableCache() + .WithTimeToLive(TimeSpan.FromHours(1)) + .Include(x => x.Stores); + } +} +```` + +We can create a simple extension method on the specification builder: + +````csharp +public static class SpecificationBuilderExtensions +{ + public static ISpecificationBuilder WithTimeToLive(this ISpecificationBuilder @this, TimeSpan ttl) + where T : class + { + @this.Specification.SetCacheTTL(ttl); + return @this; + } +} +```` + +This extension method can only be called when chained after `SpecificationBuilderExtensions.EnableCache`. This is because `EnableCache` returns `ICacheSpecificationBuilder` which inherits from `ISpecificationBuilder`. + +```csharp +// TODO: Repository example +``` + +Finally, we need to take of some plumbing to implement both `` and ``. The class below uses `ConditionalWeakTable` to do the trick. An other solution is to create a base class that inherits from `Specification`. + +````csharp +public static class SpecificationExtentions +{ + private static readonly ConditionalWeakTable SpecificationCacheOptions = new(); + + public static void SetCacheTTL(this ISpecification spec, TimeSpan ttl) + { + SpecificationCacheOptions.AddOrUpdate(spec, new CacheOptions() { TTL = ttl }); + } + + public static TimeSpan GetCacheTTL(this ISpecification spec) + { + var opts = SpecificationCacheOptions.GetOrCreateValue(spec); + return opts?.TTL ?? TimeSpan.MaxValue; + } + + // ConditionalWeakTable need reference types; TimeSpan is a struct + private class CacheOptions + { + public TimeSpan TTL { get; set; } + } +} +```` \ No newline at end of file diff --git a/docs/features/caching.md b/docs/features/caching.md index 52488b06..a424365e 100644 --- a/docs/features/caching.md +++ b/docs/features/caching.md @@ -23,6 +23,6 @@ public class CustomerByNameWithStoresSpec : Specification, ISingleResu } ``` -The `.EnableCache` method takes in two parameters: the name of the specification and the parameters of the specification. +The `.EnableCache` method takes in two parameters: the name of the specification and the parameters of the specification. It does not include any parameters to control how the cache should behave (e.g. absolute expiration date, expiration tokens, ...). However, one could create an extension method to the specification builder in order to add this information ([example](../extensions/create-specification-builder.md)). Implementing caching will also require infrastructure such as a CachedRepository, an example of which is given in [the sample](https://github.com/ardalis/Specification/blob/2605202df4d8e40fe388732db6d8f7a3754fcc2b/sample/Ardalis.SampleApp.Infrastructure/Data/CachedCustomerRepository.cs#L13) on GitHub. The `EnableCache` method is used to inform the cache implementation that caching should be used, and to configure the `CacheKey` based on the arguments supplied. From f11fe7cfa56c0631ab66b65d82f0f9c80c3c3c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Vittorelli?= Date: Sun, 26 Sep 2021 08:51:39 +0200 Subject: [PATCH 3/4] Apply code suggestions from pull request on GitHub Co-authored-by: Steve Smith --- docs/extensions/create-specification-builder.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/extensions/create-specification-builder.md b/docs/extensions/create-specification-builder.md index c1c5fc37..e750db00 100644 --- a/docs/extensions/create-specification-builder.md +++ b/docs/extensions/create-specification-builder.md @@ -7,11 +7,12 @@ nav_order: 2 # How to create your own specification builder + How to create your own specification builder ## Example: Configure caching behaviour through specification builder extension method -In order to achieve this: +In order to achieve this (note the `.WithTimeToLive` method): ````csharp public class CustomerByNameWithStores : Specification @@ -47,10 +48,10 @@ This extension method can only be called when chained after `SpecificationBuilde // TODO: Repository example ``` -Finally, we need to take of some plumbing to implement both `` and ``. The class below uses `ConditionalWeakTable` to do the trick. An other solution is to create a base class that inherits from `Specification`. +Finally, we need to take of some plumbing to implement both `` and ``. The class below uses `ConditionalWeakTable` to do the trick. Another solution is to create a base class that inherits from `Specification`. ````csharp -public static class SpecificationExtentions +public static class SpecificationExtensions { private static readonly ConditionalWeakTable SpecificationCacheOptions = new(); From ca247cffd6aa14037dcedc74d647b38f08d25428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Vittorelli?= Date: Sun, 26 Sep 2021 10:46:34 +0200 Subject: [PATCH 4/4] docs: Introduction + Cache Repository example for specification bulider --- .../create-specification-builder.md | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/docs/extensions/create-specification-builder.md b/docs/extensions/create-specification-builder.md index e750db00..cd43422d 100644 --- a/docs/extensions/create-specification-builder.md +++ b/docs/extensions/create-specification-builder.md @@ -8,7 +8,17 @@ nav_order: 2 # How to create your own specification builder -How to create your own specification builder +The specification builder from `Ardalis.Specification` is extensible by design. In fact, the methods you can use out of the box are implemented as extension methods themselves (check out the [source code](https://github.com/ardalis/Specification/blob/main/Specification/src/Ardalis.Specification/Builder/SpecificationBuilderExtensions.cs)). Your project might have requirements that cannot be satisfied by the existing toolset of course, or you might want to simplify repetitive code in several specification constructors. Whatever your case, enhancing the default builder is easy by creating your own extension methods. + +So where do you start? A good practice is to write the thing you think you need. Say you'd like to use a builder method `WithCustomerIdAndName` that takes the `Id` and `Name` of a customer as parameters. Then just write it like so: + +````csharp +Query.AsNoTracking() + .WithCustomerIdAndName(1337, "John Doe"); +```` + +From here you can inspect the return type of the builder method you chained it to (`AsNoTracking`), and create an extension method on that interface (it doesn't need to be chained of course -- working on `Query` itself is also valid). This will most likely be `ISpecificationBuilder`, but in some cases it's an inherited inteface. The example below illustrates how extension methods on inherited interfaces allow the builder to offer specific methods in specific contexts. + ## Example: Configure caching behaviour through specification builder extension method @@ -33,22 +43,51 @@ We can create a simple extension method on the specification builder: ````csharp public static class SpecificationBuilderExtensions { - public static ISpecificationBuilder WithTimeToLive(this ISpecificationBuilder @this, TimeSpan ttl) + public static ISpecificationBuilder WithTimeToLive(this ICacheSpecificationBuilder @this, TimeSpan ttl) where T : class { + // The .SetCacheTTL method is an extension method which is discussed below @this.Specification.SetCacheTTL(ttl); return @this; } } ```` -This extension method can only be called when chained after `SpecificationBuilderExtensions.EnableCache`. This is because `EnableCache` returns `ICacheSpecificationBuilder` which inherits from `ISpecificationBuilder`. +This extension method can only be called when chained after `EnableCache`. This is because `EnableCache` returns `ICacheSpecificationBuilder` which inherits from `ISpecificationBuilder`. Which is nice because it helps the IDE to give the right suggestions in the right place, and because it avoids confusing code as the `.WithTimeToLive` cannot be used without its *parent* `EnableCache` method. + +The next thing we need to is use the TTL information in a repository. For example: ```csharp -// TODO: Repository example +public class Repository +{ + private DbContext _ctx; + private MemoryCache _cache; + + public List List(ISpecification spec) + { + var specificationResult = SpecificationEvaluator.Default.GetQuery(_ctx.Set().AsQueryable(), spec); + + if (spec.CacheEnabled) + { + // The .GetCacheTTL method is an extension method which is discussed below + var ttl = spec.GetCacheTTL(); + + // Uses Microsoft's MemoryCache to cache the result + _cache.GetOrCreate(spec.CacheKey, ce => + { + ce.AbsoluteExpiration = DateTime.Now.Add(ttl); + return specificationResult.ToList(); + }); + } + else + { + return specificationResult.ToList(); + } + } +} ``` -Finally, we need to take of some plumbing to implement both `` and ``. The class below uses `ConditionalWeakTable` to do the trick. Another solution is to create a base class that inherits from `Specification`. +Finally, we need to take care of some plumbing to implement the `.GetCacheTTL` and `.SetCacheTTL` methods that we've used in the example repository and builder extension. The class below uses `ConditionalWeakTable` to do the trick. Another solution is to create a base class that inherits from `Specification`. ````csharp public static class SpecificationExtensions