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

Adding docs #182

Merged
merged 1 commit into from
Jun 12, 2022
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
6 changes: 6 additions & 0 deletions docs/_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
remote_theme: pmarsceill/just-the-docs

# Aux links for the upper right navigation
aux_links:
"Ardalis.Specification on GitHub":
- "//github.com/ardalis/specification"
8 changes: 8 additions & 0 deletions docs/extensions/extend-define-evaluators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
layout: default
title: How to extend or define your own evaluators
parent: Extensions
nav_order: 3
---

How to extend or define your own evaluators
105 changes: 105 additions & 0 deletions docs/extensions/extend-specification-builder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
layout: default
title: How to create your own specification builder
parent: Extensions
nav_order: 2
---


# How to add extensions to the 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.

````csharp
public static class SpecificationExtensions
{
public static void SetCacheTTL<T>(this ISpecification<T> spec, TimeSpan timeToLive)
{
spec.Items["CacheTTL"] = timeToLive;
}
public static TimeSpan GetCacheTTL<T>(this ISpecification<T> spec)
{
spec.Items.TryGetValue("CacheTTL", out var ttl);
return (ttl as TimeSpan?) ?? TimeSpan.MaxValue;
}
}
````
8 changes: 8 additions & 0 deletions docs/extensions/extensions-for-specifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
layout: default
title: How to write extensions to specifications
parent: Extensions
nav_order: 1
---

How to write extensions to specifications
8 changes: 8 additions & 0 deletions docs/extensions/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
layout: default
title: Extensions
nav_order: 5
has_children: true
---

How to extend the package's base functionality using extensions, builders, and evaluators.
Binary file added docs/favicon.ico
Binary file not shown.
38 changes: 38 additions & 0 deletions docs/features/asnotracking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
layout: default
title: AsNoTracking
nav_order: 1
has_children: false
parent: ORM-Specific Features
grand_parent: Features
---

# AsNoTracking

Compatible with:

- [EF Core](https://www.nuget.org/packages/Ardalis.Specification.EntityFrameworkCore/)
- [EF6](https://www.nuget.org/packages/Ardalis.Specification.EntityFramework6/)

The `AsNoTracking` feature applies this method to the resulting query executed by [EF6](https://docs.microsoft.com/en-us/dotnet/api/system.data.entity.dbextensions.asnotracking) or [EF Core](https://docs.microsoft.com/en-us/ef/core/querying/tracking#no-tracking-queries).

> No tracking queries are useful when the results are used in a read-only scenario. They're quicker to execute because there's no need to set up the change tracking information. If you don't need to update the entities retrieved from the database, then a no-tracking query should be used.

## Example

The following example shows how to add `AsNoTracking` to a specification:

```csharp
public class CustomerByNameReadOnlySpec : Specification<Customer>
{
public CustomerByNameReadOnlySpec(string name)
{
Query.Where(x => x.Name == name)
.AsNoTracking()
.OrderBy(x => x.Name)
.ThenByDescending(x => x.Address);
}
}
```

**Note:** It's a good idea to note when specifications use `AsNoTracking` so that consumers of the specification will not attempt to modify and save entities returned by queries using the specification. The above specification adds `ReadOnly` to its name for this purpose.
37 changes: 37 additions & 0 deletions docs/features/asnotrackingwithidentityresolution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
layout: default
title: AsNoTrackingWithIdentityResolution
nav_order: 2
has_children: false
parent: ORM-Specific Features
grand_parent: Features
---

# AsNoTrackingWithIdentityResolution

Compatible with:

- [EF Core](https://www.nuget.org/packages/Ardalis.Specification.EntityFrameworkCore/)

The `AsNoTrackingWithIdentityResolution` feature applies this method to the resulting query executed by[EF Core](https://docs.microsoft.com/en-us/ef/core/change-tracking/identity-resolution#identity-resolution-and-queries). It is not supported by EF 6.

> No-tracking queries can be forced to perform identity resolution by using `AsNoTrackingWithIdentityResolution<TEntity>(IQueryable<TEntity>)`. The query will then keep track of returned instances (without tracking them in the normal way) and ensure no duplicates are created in the query results.

## Example

The following example shows how to add `AsNoTrackingWithIdentityResolution` to a specification:

```csharp
public class CustomerByNameReadOnlySpec : Specification<Customer>
{
public CustomerByNameReadOnlySpec(string name)
{
Query.Where(x => x.Name == name)
.AsNoTrackingWithIdentityResolution()
.OrderBy(x => x.Name)
.ThenByDescending(x => x.Address);
}
}
```

**Note:** It's a good idea to note when specifications use `AsNoTracking` (or `AsNoTrackingWithIdentityResolution`) so that consumers of the specification will not attempt to modify and save entities returned by queries using the specification. The above specification adds `ReadOnly` to its name for this purpose.
35 changes: 35 additions & 0 deletions docs/features/assplitquery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
layout: default
title: AsSplitQuery
nav_order: 3
has_children: false
parent: ORM-Specific Features
grand_parent: Features
---

# AsSplitQuery

Compatible with:

- [EF Core](https://www.nuget.org/packages/Ardalis.Specification.EntityFrameworkCore/)

[EF Core 5 introduced support for split queries](https://docs.microsoft.com/ef/core/querying/single-split-queries#split-queries-1) which will perform separate queries rather than complex joins when returning data from multiple tables. A single query result with data from many tables may result in a "cartesian explosion" of duplicate data across many columns and rows.

> EF allows you to specify that a given LINQ query should be split into multiple SQL queries. Instead of JOINs, split queries generate an additional SQL query for each included collection navigation.

## Example

Below is a specification that uses `AsSplitQuery` in order to generate several separate queries rather than a large join across the Company, Store, and Product tables:

```csharp
public class CompanyByIdAsSplitQuery : Specification<Company>, ISingleResultSpecification
{
public CompanyByIdAsSplitQuery(int id)
{
Query.Where(company => company.Id == id)
.Include(x => x.Stores)
.ThenInclude(x => x.Products)
.AsSplitQuery();
}
}
```
11 changes: 11 additions & 0 deletions docs/features/base-features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
layout: default
title: Base Features
nav_order: 1
has_children: true
parent: Features
---

# Base Features

The features described in the docs below all work as they do in Linq. For explanations beyond those provided below, you may find the Methods section of the [Linq docs](https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable?view=net-5.0) helpful.
28 changes: 28 additions & 0 deletions docs/features/caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
layout: default
title: Caching
nav_order: 6
has_children: false
parent: Base Features
grand_parent: Features
---

# Caching

To implement caching using Specification, you will need to enable caching on your specification when it is defined:

```csharp
public class CustomerByNameWithStoresSpec : Specification<Customer>, ISingleResultSpecification
{
public CustomerByNameWithStoresSpec(string name)
{
Query.Where(x => x.Name == name)
.Include(x => x.Stores)
.EnableCache(nameof(CustomerByNameWithStoresSpec), name);
}
}
```

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/extend-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.
44 changes: 44 additions & 0 deletions docs/features/evaluate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
layout: default
title: Evaluate
nav_order: 8
has_children: false
parent: Base Features
grand_parent: Features
---

# Evaluate

Apply a specification to an in memory collection.

## Example

First, a Specification can be defined to filter a given type. In this case, a specification that filters strings using a Contains clause.

```csharp
public class StringsWhereValueContainsSpec : Specification<string>
{
public StringsWhereValueContainsSpec(string filter)
{
Query.Where(s => s.Contains(filter));
}
}
```

You can apply the Specification above to an in memory collection using the `Evaluate` method. This method takes an `IEnumerable<T>` as a parameter representing the collection to apply the specification. A brief example is demonstrated below.

```csharp
var trainingResources = new[]
{
"Articles",
"Blogs",
"Documentation",
"Pluralsight",
};

var specification = new StringsWhereValueContainsSpec("ti");

var results = specification.Evaluate(trainingResources);
```

The result of `Evaluate` should be a collection of strings containing "Articles" and "Documentation". For additional information on `Evaluate` refer to the [Specifications with In Memory Collections](../usage/use-specification-inmemory-collection.md) guide.
32 changes: 32 additions & 0 deletions docs/features/ignorequeryfilters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
layout: default
title: IgnoreQueryFilters
nav_order: 4
has_children: false
parent: ORM-Specific Features
grand_parent: Features
---

# IgnoreQueryFilters

Compatible with:

- [EF Core](https://www.nuget.org/packages/Ardalis.Specification.EntityFrameworkCore/)

The `IgnoreQueryFilters` feature is used to indicate to EF Core (it is not supported by EF 6) that it should ignore global query filters for this query. It simply passes along this call to the underlying [EF Core feature for disabling global filters](https://docs.microsoft.com/ef/core/querying/filters#disabling-filters).

## Example

The following specification implements the `IgnoreQueryFilters()` expression:

```csharp
public class CompanyByIdIgnoreQueryFilters : Specification<Company>, ISingleResultSpecification
{
public CompanyByIdIgnoreQueryFilters(int id)
{
Query
.Where(company => company.Id == id)
.IgnoreQueryFilters();
}
}
```
Loading