Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs builder extensions #162 #163

Merged
merged 4 commits into from
Sep 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 107 additions & 1 deletion docs/extensions/create-specification-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,110 @@ parent: Extensions
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<T>`, 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

In order to achieve this (note the `.WithTimeToLive` method):

````csharp
public class CustomerByNameWithStores : Specification<Customer>
{
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<T> WithTimeToLive<T>(this ICacheSpecificationBuilder<T> @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 `EnableCache`. This is because `EnableCache` returns `ICacheSpecificationBuilder<T>` which inherits from `ISpecificationBuilder<T>`. 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
public class Repository<T>
{
private DbContext _ctx;
private MemoryCache _cache;

public List<T> List(ISpecification<T> spec)
{
var specificationResult = SpecificationEvaluator.Default.GetQuery(_ctx.Set<T>().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 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<T>`.

````csharp
public static class SpecificationExtensions
{
private static readonly ConditionalWeakTable<object, CacheOptions> SpecificationCacheOptions = new();

public static void SetCacheTTL<T>(this ISpecification<T> spec, TimeSpan ttl)
{
SpecificationCacheOptions.AddOrUpdate(spec, new CacheOptions() { TTL = ttl });
}

public static TimeSpan GetCacheTTL<T>(this ISpecification<T> 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; }
}
}
````
2 changes: 1 addition & 1 deletion docs/features/caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ public class CustomerByNameWithStoresSpec : Specification<Customer>, 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.
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down