From 5668c6fd514198b3df8d686fa753d7e9c164564c Mon Sep 17 00:00:00 2001 From: Neil Brommer Date: Wed, 12 Jul 2023 11:17:20 -0700 Subject: [PATCH 1/6] Minor styling changes --- src/css/Components/_card.scss | 5 +++-- src/css/Components/_code.scss | 6 ++++++ src/css/Components/_headings.scss | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/css/Components/_card.scss b/src/css/Components/_card.scss index 61e6b0b..9e6ee82 100644 --- a/src/css/Components/_card.scss +++ b/src/css/Components/_card.scss @@ -4,12 +4,13 @@ display: flex; flex-direction: column; padding: 1rem; - margin-bottom: 1rem; + margin-top: 0.5rem; + margin-bottom: 0.5rem; border: solid 1px var(--primary-border-color); border-radius: var(--main-border-radius); h1, h2, h3, h4, h5, h6 { - margin-bottom: 0; + margin: 0; } .card-links { diff --git a/src/css/Components/_code.scss b/src/css/Components/_code.scss index 5c10228..66710d8 100644 --- a/src/css/Components/_code.scss +++ b/src/css/Components/_code.scss @@ -19,6 +19,7 @@ code { } pre { + font-size: 0.9em; background: var(--code-background); padding: 1em; border: solid 1px var(--primary-border-color); @@ -30,6 +31,11 @@ pre { border: unset; border-radius: unset; } + + &[class*=language-] { + font-size: 0.9em; + margin-bottom: 1em; + } } .token.keyword { diff --git a/src/css/Components/_headings.scss b/src/css/Components/_headings.scss index 01f21c1..cd4c8c1 100644 --- a/src/css/Components/_headings.scss +++ b/src/css/Components/_headings.scss @@ -1,8 +1,8 @@ h1, h2, h3, h4, h5, h6 { font-weight: 500; - margin-top: 0; } h1 { font-size: 3em; + margin-top: 0; } From 51299d924d1cc8d285b9e2017f5477388681e28a Mon Sep 17 00:00:00 2001 From: Neil Brommer Date: Wed, 12 Jul 2023 14:14:08 -0700 Subject: [PATCH 2/6] Make code font size similar to regular text --- src/css/Components/_code.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/css/Components/_code.scss b/src/css/Components/_code.scss index 66710d8..4bed014 100644 --- a/src/css/Components/_code.scss +++ b/src/css/Components/_code.scss @@ -12,6 +12,7 @@ $code-background-color-dark: lighten(variables.$background-color-dark, 5%); } code { + font-size: 0.9em; background: var(--code-background); padding: 0.125em 0.25em; border: solid 1px var(--primary-border-color); From be1bfe6cf36053e7d5ac254f1ce6a0f531165642 Mon Sep 17 00:00:00 2001 From: Neil Brommer Date: Wed, 12 Jul 2023 14:46:34 -0700 Subject: [PATCH 3/6] Add Markdown table of contents plugin --- eleventy.config.js | 7 +++- package-lock.json | 96 ++++++++++++++++++++++++++++++++++++++-------- package.json | 2 + 3 files changed, 88 insertions(+), 17 deletions(-) diff --git a/eleventy.config.js b/eleventy.config.js index 0a6c26c..f680196 100644 --- a/eleventy.config.js +++ b/eleventy.config.js @@ -2,6 +2,8 @@ const eleventyNavigationPlugin = require("@11ty/eleventy-navigation"); const eleventySyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight"); const eleventySass = require("eleventy-sass"); const mdDefList = require("markdown-it-deflist"); +const mdToc = require("markdown-it-table-of-contents"); +const mdAnchor = require("markdown-it-anchor"); const eleventyRss = require("@11ty/eleventy-plugin-rss"); module.exports = function (eleventyConfig) { @@ -24,7 +26,10 @@ module.exports = function (eleventyConfig) { loadPaths: ["node_modules"], }, }); - eleventyConfig.amendLibrary("md", mdLib => mdLib.use(mdDefList)); + eleventyConfig.amendLibrary("md", mdLib => mdLib + .use(mdDefList) + .use(mdToc) + .use(mdAnchor)); eleventyConfig.addFilter("IsNotPage", (collection, url) => collection.filter(item => item.url != url)); diff --git a/package-lock.json b/package-lock.json index a74228a..4592377 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "dependencies": { "@11ty/eleventy-plugin-rss": "^1.2.0", "feather-icons": "^4.29.0", + "markdown-it-anchor": "^8.6.7", + "markdown-it-table-of-contents": "^0.6.0", "normalize.css": "^8.0.1", "prism-themes": "^1.9.0" }, @@ -298,6 +300,28 @@ "node": ">=8" } }, + "node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "peer": true + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "peer": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "peer": true + }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -1411,7 +1435,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", - "dev": true, "dependencies": { "uc.micro": "^1.0.1" } @@ -1481,7 +1504,6 @@ "version": "13.0.1", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", - "dev": true, "dependencies": { "argparse": "^2.0.1", "entities": "~3.0.1", @@ -1493,17 +1515,33 @@ "markdown-it": "bin/markdown-it.js" } }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, "node_modules/markdown-it-deflist": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.1.0.tgz", "integrity": "sha512-3OuqoRUlSxJiuQYu0cWTLHNhhq2xtoSFqsZK8plANg91+RJQU1ziQ6lA2LzmFAEes18uPBsHZpcX6We5l76Nzg==", "dev": true }, + "node_modules/markdown-it-table-of-contents": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.6.0.tgz", + "integrity": "sha512-jHvEjZVEibyW97zEYg19mZCIXO16lHbvRaPDkEuOfMPBmzlI7cYczMZLMfUvwkhdOVQpIxu3gx6mgaw46KsNsQ==", + "engines": { + "node": ">6.4.0" + } + }, "node_modules/markdown-it/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/maximatch": { "version": "0.1.0", @@ -1553,8 +1591,7 @@ "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", - "dev": true + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" }, "node_modules/merge2": { "version": "1.4.1", @@ -2353,8 +2390,7 @@ "node_modules/uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "dev": true + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, "node_modules/uglify-js": { "version": "3.15.3", @@ -2654,6 +2690,28 @@ } } }, + "@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "peer": true + }, + "@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "peer": true, + "requires": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "peer": true + }, "@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -3479,7 +3537,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", - "dev": true, "requires": { "uc.micro": "^1.0.1" } @@ -3531,7 +3588,6 @@ "version": "13.0.1", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", - "dev": true, "requires": { "argparse": "^2.0.1", "entities": "~3.0.1", @@ -3543,17 +3599,27 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" } } }, + "markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "requires": {} + }, "markdown-it-deflist": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.1.0.tgz", "integrity": "sha512-3OuqoRUlSxJiuQYu0cWTLHNhhq2xtoSFqsZK8plANg91+RJQU1ziQ6lA2LzmFAEes18uPBsHZpcX6We5l76Nzg==", "dev": true }, + "markdown-it-table-of-contents": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.6.0.tgz", + "integrity": "sha512-jHvEjZVEibyW97zEYg19mZCIXO16lHbvRaPDkEuOfMPBmzlI7cYczMZLMfUvwkhdOVQpIxu3gx6mgaw46KsNsQ==" + }, "maximatch": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/maximatch/-/maximatch-0.1.0.tgz", @@ -3592,8 +3658,7 @@ "mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", - "dev": true + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" }, "merge2": { "version": "1.4.1", @@ -4204,8 +4269,7 @@ "uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "dev": true + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, "uglify-js": { "version": "3.15.3", diff --git a/package.json b/package.json index 66d3400..2343d0c 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "dependencies": { "@11ty/eleventy-plugin-rss": "^1.2.0", "feather-icons": "^4.29.0", + "markdown-it-anchor": "^8.6.7", + "markdown-it-table-of-contents": "^0.6.0", "normalize.css": "^8.0.1", "prism-themes": "^1.9.0" } From f1c6ac09c84d1a7539e5c31655e9cc21c56b6ae9 Mon Sep 17 00:00:00 2001 From: Neil Brommer Date: Wed, 12 Jul 2023 15:25:58 -0700 Subject: [PATCH 4/6] Use CSS to capitalize headings --- src/css/Components/_headings.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/css/Components/_headings.scss b/src/css/Components/_headings.scss index cd4c8c1..d5906c4 100644 --- a/src/css/Components/_headings.scss +++ b/src/css/Components/_headings.scss @@ -1,5 +1,6 @@ h1, h2, h3, h4, h5, h6 { font-weight: 500; + text-transform: capitalize; } h1 { From 8e541f28dd941abc8ff1512e7543713877aeb5bf Mon Sep 17 00:00:00 2001 From: Neil Brommer Date: Wed, 12 Jul 2023 15:28:16 -0700 Subject: [PATCH 5/6] Add styling for table of contents --- eleventy.config.js | 4 +++- src/css/Components/_table-of-contents.scss | 10 ++++++++++ src/css/site.scss | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/css/Components/_table-of-contents.scss diff --git a/eleventy.config.js b/eleventy.config.js index f680196..04325ea 100644 --- a/eleventy.config.js +++ b/eleventy.config.js @@ -28,7 +28,9 @@ module.exports = function (eleventyConfig) { }); eleventyConfig.amendLibrary("md", mdLib => mdLib .use(mdDefList) - .use(mdToc) + .use(mdToc, { + includeLevel: [ 1, 2, 3, 4, 5, 6 ] + }) .use(mdAnchor)); eleventyConfig.addFilter("IsNotPage", (collection, url) => diff --git a/src/css/Components/_table-of-contents.scss b/src/css/Components/_table-of-contents.scss new file mode 100644 index 0000000..540247d --- /dev/null +++ b/src/css/Components/_table-of-contents.scss @@ -0,0 +1,10 @@ +@use '_variables'; + +.table-of-contents { + padding-left: 0.5em; + padding-right: 1em; + border: 1px solid var(--primary-border-color); + border-radius: var(--main-border-radius); + + text-transform: capitalize; +} diff --git a/src/css/site.scss b/src/css/site.scss index fdfd2bb..882e465 100644 --- a/src/css/site.scss +++ b/src/css/site.scss @@ -12,3 +12,4 @@ @use 'Components/_lists'; @use 'Components/_code'; @use 'Components/_posts'; +@use 'Components/_table-of-contents'; From e12287cc9590da6401f7e08b48ab5ac7e76aa2d9 Mon Sep 17 00:00:00 2001 From: Neil Brommer Date: Wed, 12 Jul 2023 15:28:31 -0700 Subject: [PATCH 6/6] Add post on pluggable queries --- src/posts/PluggableQueries.md | 167 ++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 src/posts/PluggableQueries.md diff --git a/src/posts/PluggableQueries.md b/src/posts/PluggableQueries.md new file mode 100644 index 0000000..8f956b1 --- /dev/null +++ b/src/posts/PluggableQueries.md @@ -0,0 +1,167 @@ +--- +title: Pluggable Expressions In Entity Framework +description: The application structure I've been using as an alternative to the repository pattern +tags: [ Programming, C#, Entity Framework ] +--- + +[[toc]] + +After maintaining a mid-sized application using a repository pattern, I've found this method to be more flexible and easier to test. + +## Pluggable expressions + +Expressions can be saved to variables (or returned by properties or functions) just like any object and just referenced in a query: + +```csharp +Expression> registrationIsOpen = + t => t.RegistrationOpenDate < DateTime.Now + && t.RegistrationCloseDate > DateTime.Now; + +IList openThings = db.Things + .Where(registrationIsOpen) + .ToList(); +``` + +This also has the benefit of being very declarative; it's pretty easy to tell what the query is doing with `.Where(registrationIsOpen)` at a glance, even if you don't know much about `Thing` or the database structure. This becomes more of an advantage the more complicated the filter is. + +These are also easy to test. Just create an in-memory list, run the expression against the list, and verify the results. Like with all EF queries though some expressions can't be translated, so you need to watch out for that. + +You can have static classes filled with a bunch of pre-built expressions that you can pick from and apply as needed. If you have a one-off filter you need to use, then you can just directly use a lambda instead of a pre-built expression. + +This can also be used on anything that `Where` can be called on. So if you already have a list, you can just reuse the same expression in a LINQ query on that list. Another useful case for this is map functions. Just like you can create expressions to pass to `.Where()`, you can also create expressions to pass to `.Select()`: + +```csharp +public class ThingViewModel { + public int ThingId { get; set; } + public string ThingName { get; set; } + public DateTime RegistrationOpenDate { get; set; } + public DateTime RegistrationCloseDate { get; set; } + ... + + public static Expression> Map() { + return t => new ThingViewModel { + ThingId = t.ThingId, + ThingName = t.ThingName, + RegistrationOpenDate = t.RegistrationOpenDate, + RegistrationCloseDate = t.RegistrationCloseDate, + ... + }; + } +} +``` + +Then to use it: + +```csharp +IList things = db.Things + .Select(ThingViewModel.Map()) + .ToList(); +``` + +## Nested expressions + +EF doesn't actually execute the LINQ query - it uses reflection to pick the query apart and translate it into a SQL query. Because of this, EF can only translate function calls and expressions it recognizes. This becomes a problem when you have nested expressions like this: + +```csharp +Expression> isPastRegistrationOpenDate = + t => t.RegistrationOpenDate < DateTime.Now; +Expression> isBeforeRegistrationCloseDate = + t => t.RegistrationCloseDate > DateTime.Now; +Expression> registrationIsOpen = + t => isPastRegistrationOpenDate.Invoke(t) + && isBeforeRegistrationCloseDate.Invoke(t); + +IList openThings = db.Things + .Where(registrationIsOpen) + .ToList(); +``` + +This query will fail because Entity Framework can't doesn't have a translation for `isPastRegistrationOpenDate` and `isBeforeRegistrationCloseDate`. + +In this case, the `registrationIsOpen` in `.Where(registrationIsOpen)` is replaced by the actual expression and EF will read the actual underlying expression. However, what EF sees when it tries to translate the query is references to two variables and an `Invoke` function that it doesn't understand. + +The workaround for this is to use [LINQKit's `.AsExpandable()`](https://github.com/scottksmith95/LINQKit#plugging-expressions-into-entitysets--entitycollections-the-solution). By adding this to the query, it will alter the query and replace ("expand") the references to expressions with the expressions themselves. Then EF will see expressions that it understands. + +So unlike the query above, this will work: + +```csharp +Expression> isPastRegistrationOpenDate = + t => t.RegistrationOpenDate < DateTime.Now; +Expression> isBeforeRegistrationCloseDate = + t => t.RegistrationCloseDate > DateTime.Now; +Expression> registrationIsOpen = + t => isPastRegistrationOpenDate.Invoke(t) + && isBeforeRegistrationCloseDate.Invoke(t); + +IList openThings = db.Things + .AsExpandable() + .Where(registrationIsOpen) + .ToList(); +``` + +And that's it. Just add `.AsExpandable()` to your query and it will work. + +Another example using the above view model example: + +```csharp +public static class ThingExpressions { + public static Expression> IsPastRegistrationOpenDate() { + return t => t.RegistrationOpenDate < DateTime.Now; + } + + public static Expression> IsBeforeRegistrationCloseDate() { + return t => t.RegistrationCloseDate > DateTime.Now; + } + + public static Expression> RegistrationIsOpen() { + return t => IsPastRegistrationOpenDate.Invoke(t) + && IsBeforeRegistrationCloseDate.Invoke(t); + } +} + +public class ThingViewModel { + public int ThingId { get; set; } + public string ThingName { get; set; } + public bool IsRegistrationOpen { get; set; } + + public static Expression> Map() { + return t => new ThingViewModel { + ThingId = t.ThingId, + ThingName = t.ThingName, + // This will throw an exception if AsExpandable isn't used + IsRegistrationOpen = ThingExpressions.RegistrationIsOpen().Invoke(t) + }; + } +} +``` + +Then to use it: + +```csharp +IList things = db.Things + .AsExpandable() + .Select(ThingViewModel.Map()) + .ToList(); +``` + +## Compared to repositories + +The usual way to centralize filters for database queries with Entity Framework is to use repositories with methods like this: + +```csharp +public IList GetThings(bool whereRegistrationOpen = false) { + var query = this.db.Things + .AsQueryable(); + + if (whereRegistrationOpen) + query = query + .Where(t => t.RegistrationOpenDate < DateTime.Now) + .Where(t => t.RegistrationCloseDate > DateTime.Now); + + return query.ToList(); +} +``` + +However, for large, complex objects this can quickly bloat the function with dozens of options. This makes the method very large (potentially hundreds of lines) and more awkward to ensure that tests each only test a specific piece of the method. + +Also, the filters themselves are sealed in the repository method. You can't use them in a different way than the repository is built for (like applying the filter to an in-memory list or using them to set a property in a `.Select()`).