diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..d3985ef --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,271 @@ +.highlight table td { padding: 5px; } + +.highlight table pre { margin: 0; } + +.highlight .cm { color: #777772; font-style: italic; } + +.highlight .cp { color: #797676; font-weight: bold; } + +.highlight .c1 { color: #777772; font-style: italic; } + +.highlight .cs { color: #797676; font-weight: bold; font-style: italic; } + +.highlight .c, .highlight .cd { color: #777772; font-style: italic; } + +.highlight .err { color: #a61717; background-color: #e3d2d2; } + +.highlight .gd { color: #000000; background-color: #ffdddd; } + +.highlight .ge { color: #000000; font-style: italic; } + +.highlight .gr { color: #aa0000; } + +.highlight .gh { color: #797676; } + +.highlight .gi { color: #000000; background-color: #ddffdd; } + +.highlight .go { color: #888888; } + +.highlight .gp { color: #555555; } + +.highlight .gs { font-weight: bold; } + +.highlight .gu { color: #aaaaaa; } + +.highlight .gt { color: #aa0000; } + +.highlight .kc { color: #000000; font-weight: bold; } + +.highlight .kd { color: #000000; font-weight: bold; } + +.highlight .kn { color: #000000; font-weight: bold; } + +.highlight .kp { color: #000000; font-weight: bold; } + +.highlight .kr { color: #000000; font-weight: bold; } + +.highlight .kt { color: #445588; font-weight: bold; } + +.highlight .k, .highlight .kv { color: #000000; font-weight: bold; } + +.highlight .mf { color: #009999; } + +.highlight .mh { color: #009999; } + +.highlight .il { color: #009999; } + +.highlight .mi { color: #009999; } + +.highlight .mo { color: #009999; } + +.highlight .m, .highlight .mb, .highlight .mx { color: #009999; } + +.highlight .sb { color: #d14; } + +.highlight .sc { color: #d14; } + +.highlight .sd { color: #d14; } + +.highlight .s2 { color: #d14; } + +.highlight .se { color: #d14; } + +.highlight .sh { color: #d14; } + +.highlight .si { color: #d14; } + +.highlight .sx { color: #d14; } + +.highlight .sr { color: #009926; } + +.highlight .s1 { color: #d14; } + +.highlight .ss { color: #990073; } + +.highlight .s { color: #d14; } + +.highlight .na { color: #008080; } + +.highlight .bp { color: #797676; } + +.highlight .nb { color: #0086B3; } + +.highlight .nc { color: #445588; font-weight: bold; } + +.highlight .no { color: #008080; } + +.highlight .nd { color: #3c5d5d; font-weight: bold; } + +.highlight .ni { color: #800080; } + +.highlight .ne { color: #990000; font-weight: bold; } + +.highlight .nf { color: #990000; font-weight: bold; } + +.highlight .nl { color: #990000; font-weight: bold; } + +.highlight .nn { color: #555555; } + +.highlight .nt { color: #000080; } + +.highlight .vc { color: #008080; } + +.highlight .vg { color: #008080; } + +.highlight .vi { color: #008080; } + +.highlight .nv { color: #008080; } + +.highlight .ow { color: #000000; font-weight: bold; } + +.highlight .o { color: #000000; font-weight: bold; } + +.highlight .w { color: #bbbbbb; } + +.highlight { background-color: #f8f8f8; } + +/******************************************************************************* +MeyerWeb Reset +*******************************************************************************/ +html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font: inherit; vertical-align: baseline; } + +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; } + +ol, ul { list-style: none; } + +table { border-collapse: collapse; border-spacing: 0; } + +/******************************************************************************* +Theme Styles +*******************************************************************************/ +body { box-sizing: border-box; color: #373737; background: #212121; font-size: 16px; font-family: 'Myriad Pro', Calibri, Helvetica, Arial, sans-serif; line-height: 1.5; -webkit-font-smoothing: antialiased; } + +h1, h2, h3, h4, h5, h6 { margin: 10px 0; font-weight: 700; color: #222222; font-family: 'Lucida Grande', 'Calibri', Helvetica, Arial, sans-serif; letter-spacing: -1px; } + +h1 { font-size: 36px; font-weight: 700; } + +h2 { padding-bottom: 10px; font-size: 32px; background: url("../images/bg_hr.png") repeat-x bottom; } + +h3 { font-size: 24px; } + +h4 { font-size: 21px; } + +h5 { font-size: 18px; } + +h6 { font-size: 16px; } + +p { margin: 10px 0 15px 0; } + +footer p { color: #f2f2f2; } + +a { text-decoration: none; color: #0F79D0; text-shadow: none; transition: color 0.5s ease; transition: text-shadow 0.5s ease; -webkit-transition: color 0.5s ease; -webkit-transition: text-shadow 0.5s ease; -moz-transition: color 0.5s ease; -moz-transition: text-shadow 0.5s ease; -o-transition: color 0.5s ease; -o-transition: text-shadow 0.5s ease; -ms-transition: color 0.5s ease; -ms-transition: text-shadow 0.5s ease; } + +a:hover, a:focus { text-decoration: underline; } + +footer a { color: #F2F2F2; text-decoration: underline; } + +em, cite { font-style: italic; } + +strong { font-weight: bold; } + +img { position: relative; margin: 0 auto; max-width: 739px; padding: 5px; margin: 10px 0 10px 0; border: 1px solid #ebebeb; box-shadow: 0 0 5px #ebebeb; -webkit-box-shadow: 0 0 5px #ebebeb; -moz-box-shadow: 0 0 5px #ebebeb; -o-box-shadow: 0 0 5px #ebebeb; -ms-box-shadow: 0 0 5px #ebebeb; } + +p img { display: inline; margin: 0; padding: 0; vertical-align: middle; text-align: center; border: none; } + +pre, code { color: #222; background-color: #fff; font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace; font-size: 14px; border-radius: 2px; -moz-border-radius: 2px; -webkit-border-radius: 2px; } + +pre { padding: 10px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); overflow: auto; } + +code { padding: 3px; margin: 0 3px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } + +pre code { display: block; box-shadow: none; } + +blockquote { color: #666; margin-bottom: 20px; padding: 0 0 0 20px; border-left: 3px solid #bbb; } + +ul, ol, dl { margin-bottom: 15px; } + +ul { list-style-position: inside; list-style: disc; padding-left: 20px; } + +ol { list-style-position: inside; list-style: decimal; padding-left: 20px; } + +dl dt { font-weight: bold; } + +dl dd { padding-left: 20px; font-style: italic; } + +dl p { padding-left: 20px; font-style: italic; } + +hr { height: 1px; margin-bottom: 5px; border: none; background: url("../images/bg_hr.png") repeat-x center; } + +table { border: 1px solid #373737; margin-bottom: 20px; text-align: left; } + +th { font-family: 'Lucida Grande', 'Helvetica Neue', Helvetica, Arial, sans-serif; padding: 10px; background: #373737; color: #fff; } + +td { padding: 10px; border: 1px solid #373737; } + +form { background: #f2f2f2; padding: 20px; } + +/******************************************************************************* +Full-Width Styles +*******************************************************************************/ +.outer { width: 100%; } + +.inner { position: relative; max-width: 640px; padding: 20px 10px; margin: 0 auto; } + +#forkme_banner { display: block; position: absolute; top: 0; right: 10px; z-index: 10; padding: 10px 50px 10px 10px; color: #fff; background: url("../images/blacktocat.png") #0090ff no-repeat 95% 50%; font-weight: 700; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); border-bottom-left-radius: 2px; border-bottom-right-radius: 2px; } + +#header_wrap { background: #212121; background: -moz-linear-gradient(top, #373737, #212121); background: -webkit-linear-gradient(top, #373737, #212121); background: -ms-linear-gradient(top, #373737, #212121); background: -o-linear-gradient(top, #373737, #212121); background: linear-gradient(to top, #373737, #212121); } + +#header_wrap .inner { padding: 50px 10px 30px 10px; } + +#project_title { margin: 0; color: #fff; font-size: 42px; font-weight: 700; text-shadow: #111 0px 0px 10px; } + +#project_tagline { color: #fff; font-size: 24px; font-weight: 300; background: none; text-shadow: #111 0px 0px 10px; } + +#downloads { position: absolute; width: 210px; z-index: 10; bottom: -40px; right: 0; height: 70px; background: url("../images/icon_download.png") no-repeat 0% 90%; } + +.zip_download_link { display: block; float: right; width: 90px; height: 70px; text-indent: -5000px; overflow: hidden; background: url(../images/sprite_download.png) no-repeat bottom left; } + +.tar_download_link { display: block; float: right; width: 90px; height: 70px; text-indent: -5000px; overflow: hidden; background: url(../images/sprite_download.png) no-repeat bottom right; margin-left: 10px; } + +.zip_download_link:hover { background: url(../images/sprite_download.png) no-repeat top left; } + +.tar_download_link:hover { background: url(../images/sprite_download.png) no-repeat top right; } + +#main_content_wrap { background: #f2f2f2; border-top: 1px solid #111; border-bottom: 1px solid #111; } + +#main_content { padding-top: 40px; } + +#footer_wrap { background: #212121; } + +/******************************************************************************* +Small Device Styles +*******************************************************************************/ +@media screen and (max-width: 992px) { img { max-width: 100%; } } +@media screen and (max-width: 480px) { body { font-size: 14px; } + #downloads { display: none; } + .inner { min-width: 320px; max-width: 480px; } + #project_title { font-size: 32px; } + h1 { font-size: 28px; } + h2 { font-size: 24px; } + h3 { font-size: 21px; } + h4 { font-size: 18px; } + h5 { font-size: 14px; } + h6 { font-size: 12px; } + code, pre { font-size: 11px; } } +@media screen and (max-width: 320px) { body { font-size: 14px; } + #downloads { display: none; } + .inner { min-width: 240px; max-width: 320px; } + #project_title { font-size: 28px; } + h1 { font-size: 24px; } + h2 { font-size: 21px; } + h3 { font-size: 18px; } + h4 { font-size: 16px; } + h5 { font-size: 14px; } + h6 { font-size: 12px; } + code, pre { min-width: 240px; max-width: 320px; font-size: 11px; } } +.inner { max-width: 960px; } + +pre, code { background-color: unset; font-size: unset; } + +code { font-size: 0.80em; } diff --git a/assets/images/bg_hr.png b/assets/images/bg_hr.png new file mode 100644 index 0000000..514aee5 Binary files /dev/null and b/assets/images/bg_hr.png differ diff --git a/assets/images/blacktocat.png b/assets/images/blacktocat.png new file mode 100644 index 0000000..e160053 Binary files /dev/null and b/assets/images/blacktocat.png differ diff --git a/assets/images/icon_download.png b/assets/images/icon_download.png new file mode 100644 index 0000000..5a793f1 Binary files /dev/null and b/assets/images/icon_download.png differ diff --git a/assets/images/sponsors.png b/assets/images/sponsors.png new file mode 100644 index 0000000..f82f5bd Binary files /dev/null and b/assets/images/sponsors.png differ diff --git a/assets/images/sponsors.svg b/assets/images/sponsors.svg new file mode 100644 index 0000000..1a1d80f --- /dev/null +++ b/assets/images/sponsors.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/images/sprite_download.png b/assets/images/sprite_download.png new file mode 100644 index 0000000..f9f8de2 Binary files /dev/null and b/assets/images/sprite_download.png differ diff --git a/docs/api/index.html b/docs/api/index.html new file mode 100644 index 0000000..307a6ec --- /dev/null +++ b/docs/api/index.html @@ -0,0 +1,77 @@ + + + + + + + + + + +Avatar API | avatar + + + + + + + + + + + + + + +
+
+ View on GitHub + +

avatar

+

A modern compile-time generated interception/proxy library

+ + +
+
+ + +
+
+

Avatar API

+ +

Main entry point API is Avatar.Of<T>, which creates an object that implements T:

+ +
ICalculator calculator = Avatar.Of<ICalculator>();
+
+// Extension methods to easily add behaviors without having 
+// to cast to IAvatar
+calculator.AddBehavior((invocation, next) => ...);
+
+Console.WriteLine(calculator.Add(2, 5));
+
+ +

There are overloads for implementing additional types, as well as passing constructor +arguments if the base type T (which must be the first in the list, like in regular +C# type declarations) is a class that provides a constructor with matching parameters: +Avatar.Of<T, T1...Tn>(arg1, ... argn)

+ +

For anonymous behaviors, the delegate/lambda based overloads are typically sufficient. +For more advanced or reusable behaviors, you can implement IAvatarBehavior instead.

+ +
+
+ + + + + + + diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..5eff428 --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,21 @@ +# Avatar API + +Main entry point API is `Avatar.Of`, which creates an object that implements `T`: + +```csharp +ICalculator calculator = Avatar.Of(); + +// Extension methods to easily add behaviors without having +// to cast to IAvatar +calculator.AddBehavior((invocation, next) => ...); + +Console.WriteLine(calculator.Add(2, 5)); +``` + +There are overloads for implementing additional types, as well as passing constructor +arguments if the base type `T` (which must be the first in the list, like in regular +C# type declarations) is a class that provides a constructor with matching parameters: +`Avatar.Of(arg1, ... argn)` + +For anonymous behaviors, the delegate/lambda based overloads are typically sufficient. +For more advanced or reusable behaviors, you can implement `IAvatarBehavior` instead. diff --git a/docs/docfx.json b/docs/docfx.json new file mode 100644 index 0000000..2b786cf --- /dev/null +++ b/docs/docfx.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json.schemastore.org/docfx", + "metadata": [ + { + "src": [ + { + "files": [ "*.cs" ], + "src": "../src/Avatar" + } + ], + "dest": "docs/api", + "filter": "filter.yml" + } + ], + "build": { + "content": [ + { + "files": [ + "*.yml", + "index.md" + ], + "src": "api", + "dest": "api" + }, + + { + "files": [ + "*.md", + "toc.yml" + ] + } + ], + "resource": [ + { + "files": [ + "logo.svg", + "favicon.ico", + "favicon/*.*", + "assets/**/*.*" + ] + } + ], + "globalMetadata": { + "_appTitle": "Avatar", + "_rel": "/avatar/", + "_enableSearch": true + }, + "template": [ + "default", + "./template" + ], + "dest": "_site" + } +} diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100644 index 0000000..73e5200 Binary files /dev/null and b/docs/favicon.ico differ diff --git a/docs/favicon/android-chrome-192x192.png b/docs/favicon/android-chrome-192x192.png new file mode 100644 index 0000000..7e30979 Binary files /dev/null and b/docs/favicon/android-chrome-192x192.png differ diff --git a/docs/favicon/android-chrome-512x512.png b/docs/favicon/android-chrome-512x512.png new file mode 100644 index 0000000..30e192a Binary files /dev/null and b/docs/favicon/android-chrome-512x512.png differ diff --git a/docs/favicon/apple-touch-icon.png b/docs/favicon/apple-touch-icon.png new file mode 100644 index 0000000..148927b Binary files /dev/null and b/docs/favicon/apple-touch-icon.png differ diff --git a/docs/favicon/favicon-16x16.png b/docs/favicon/favicon-16x16.png new file mode 100644 index 0000000..4aa7669 Binary files /dev/null and b/docs/favicon/favicon-16x16.png differ diff --git a/docs/favicon/favicon-32x32.png b/docs/favicon/favicon-32x32.png new file mode 100644 index 0000000..6e271ee Binary files /dev/null and b/docs/favicon/favicon-32x32.png differ diff --git a/docs/favicon/site.webmanifest b/docs/favicon/site.webmanifest new file mode 100644 index 0000000..76d20c5 --- /dev/null +++ b/docs/favicon/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/favicon/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/favicon/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#FFFFFF", + "background_color": "#FFFFFF", + "display": "standalone" + } \ No newline at end of file diff --git a/docs/filter.yml b/docs/filter.yml new file mode 100644 index 0000000..7e10de5 --- /dev/null +++ b/docs/filter.yml @@ -0,0 +1,6 @@ +apiRules: + - exclude: + hasAttribute: + uid: System.ComponentModel.EditorBrowsableAttribute + ctorArguments: + - System.ComponentModel.EditorBrowsableState.Never diff --git a/docs/images/AddInsertBehavior.png b/docs/images/AddInsertBehavior.png new file mode 100644 index 0000000..501c33e Binary files /dev/null and b/docs/images/AddInsertBehavior.png differ diff --git a/docs/images/AvatarApi.png b/docs/images/AvatarApi.png new file mode 100644 index 0000000..81b5b0b Binary files /dev/null and b/docs/images/AvatarApi.png differ diff --git a/docs/images/AvatarIncubation.png b/docs/images/AvatarIncubation.png new file mode 100644 index 0000000..2a31a16 Binary files /dev/null and b/docs/images/AvatarIncubation.png differ diff --git a/docs/images/DebuggerDisplay.png b/docs/images/DebuggerDisplay.png new file mode 100644 index 0000000..ce0359a Binary files /dev/null and b/docs/images/DebuggerDisplay.png differ diff --git a/docs/images/DebuggingBehavior.png b/docs/images/DebuggingBehavior.png new file mode 100644 index 0000000..839bcf8 Binary files /dev/null and b/docs/images/DebuggingBehavior.png differ diff --git a/docs/images/icon-32.png b/docs/images/icon-32.png new file mode 100644 index 0000000..64f669f Binary files /dev/null and b/docs/images/icon-32.png differ diff --git a/docs/images/icon.png b/docs/images/icon.png new file mode 100644 index 0000000..53bc17d Binary files /dev/null and b/docs/images/icon.png differ diff --git a/docs/images/icon.svg b/docs/images/icon.svg new file mode 100644 index 0000000..aed7833 --- /dev/null +++ b/docs/images/icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/images/noun_alien_24531.svg b/docs/images/noun_alien_24531.svg new file mode 100644 index 0000000..b58995a --- /dev/null +++ b/docs/images/noun_alien_24531.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/noun_alien_24531_100.png b/docs/images/noun_alien_24531_100.png new file mode 100644 index 0000000..7a89df1 Binary files /dev/null and b/docs/images/noun_alien_24531_100.png differ diff --git a/docs/images/noun_alien_24531_1200.png b/docs/images/noun_alien_24531_1200.png new file mode 100644 index 0000000..bc87a86 Binary files /dev/null and b/docs/images/noun_alien_24531_1200.png differ diff --git a/docs/images/noun_alien_24531_128.png b/docs/images/noun_alien_24531_128.png new file mode 100644 index 0000000..0ae9785 Binary files /dev/null and b/docs/images/noun_alien_24531_128.png differ diff --git a/docs/images/noun_alien_24531_512.png b/docs/images/noun_alien_24531_512.png new file mode 100644 index 0000000..4e8c8a9 Binary files /dev/null and b/docs/images/noun_alien_24531_512.png differ diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..bd5a6c3 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,58 @@ + + + + + + + + + + +avatar | A modern compile-time generated interception/proxy library + + + + + + + + + + + + + + +
+
+ View on GitHub + +

avatar

+

A modern compile-time generated interception/proxy library

+ + +
+
+ + +
+
+

[!includeReadme]

+ +
+
+ + + + + + + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..28e0f0a --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +[!include[Readme](../readme.md)] \ No newline at end of file diff --git a/docs/logo.svg b/docs/logo.svg new file mode 100644 index 0000000..259cfe4 --- /dev/null +++ b/docs/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/styles/main.css b/docs/styles/main.css new file mode 100644 index 0000000..22d69f8 --- /dev/null +++ b/docs/styles/main.css @@ -0,0 +1,9 @@ +@media (min-width:1400px) { + .container { + width: 1300px; + } +} + +.table { + width: auto +} diff --git a/docs/styles/main.js b/docs/styles/main.js new file mode 100644 index 0000000..e69de29 diff --git a/docs/template/partials/head.tmpl.partial b/docs/template/partials/head.tmpl.partial new file mode 100644 index 0000000..8e8de8e --- /dev/null +++ b/docs/template/partials/head.tmpl.partial @@ -0,0 +1,27 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} + + + + + {{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}} + + + + {{#_description}}{{/_description}} + + + + + + + + + + + + + + {{#_noindex}}{{/_noindex}} + {{#_enableSearch}}{{/_enableSearch}} + {{#_enableNewTab}}{{/_enableNewTab}} + diff --git a/docs/template/partials/scripts.tmpl.partial b/docs/template/partials/scripts.tmpl.partial new file mode 100644 index 0000000..9d08b27 --- /dev/null +++ b/docs/template/partials/scripts.tmpl.partial @@ -0,0 +1,6 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} + + + + + diff --git a/docs/toc.yml b/docs/toc.yml new file mode 100644 index 0000000..6e6f2ee --- /dev/null +++ b/docs/toc.yml @@ -0,0 +1,5 @@ +- name: Home + href: index.md +- name: API + href: api/ + homepage: api/index.md diff --git a/index.html b/index.html new file mode 100644 index 0000000..edd1af4 --- /dev/null +++ b/index.html @@ -0,0 +1,214 @@ + + + + + + + + + + +avatar | A modern compile-time generated interception/proxy library + + + + + + + + + + + + + + +
+
+ View on GitHub + +

avatar

+

A modern compile-time generated interception/proxy library

+ + +
+
+ + +
+
+

Icon Avatar

+ +

Avatar is a modern interception library which implements the proxy pattern and runs everywhere, even where run-time code generation (Reflection.Emit) is forbidden or limitted, like physical iOS devices and game consoles, through compile-time code generation. The proxy behavior is configured in code using what we call a behavior pipeline.

+ +
+

Avatars blend in with the Na’vi seamlessly, and you can control their behavior precisely by ‘driving’ them through a psionic link. Just like a proxy, with behavior driven through code.

+
+ +

Avatar Overloads

+ +

Version +Downloads +License +Discord Chat +GitHub

+ +

CI Version +GH CI Status

+ +
+

NOTE: Avatar provides a fairly low-level API with just the essential building blocks on top of which higher-level APIs can be built, such as the upcoming Moq vNext API.

+
+ +

Requirements

+ +

Avatar is a .NET Standard 2.0 library and runs on any runtime that supports that.

+ +

Compile-time proxy generation leverages Roslyn source generators and therefore requires C# 9.0, which at this time is included in Visual Studio 16.8 (preview or later) and the .NET 5.0 SDK (RC or later). Compile-time generated proxies support the broadest possible run-time platforms since they don’t require any Reflection.Emit, and also don’t pay that performance cost either.

+ +

Whenever compile-time proxy generation is not available, a fallback generation strategy is used instead, which leverages Castle DynamicProxy to provide the run-time code generation.

+ +

The client API for configuring proxy behaviors in either case is exactly the same.

+ +
+

NOTE: even though generated proxies is the main usage for Avatar, the API was designed so that you can also consume the behavior pipeline easily from hand-coded proxies too.

+
+ +

Usage

+ +
ICalculator calc = Avatar.Of<ICalculator>();
+
+calc.AddBehavior((invocation, next) => ...);
+
+ +

AddBehavior/InsertBehavior overloads allow granular control of the avatar’s behavior pipeline, which is basically a chain of responsibility that invokes all configured behaviors that apply to the current invocation. Individual behaviors can determine whether to short-circuit the call or call the next behavior in the chain.

+ +

Avatar Overloads

+ +

Behaviors can also dynamically determine whether they apply to a given invocation by providing the optional appliesTo argument. In addition to the delegate-based overloads (called anonymous behaviors), you can also create behaviors by implementing the IAvatarBehavior interface:

+ +
public interface IAvatarBehavior
+{
+    bool AppliesTo(IMethodInvocation invocation);
+    IMethodReturn Execute(IMethodInvocation invocation, ExecuteHandler next);
+}
+
+ +

Common Behaviors

+ +

Some commonly used behaviors that are generally useful are provided in the library and can be added to avatars as needed:

+ +
    +
  • +

    DefaultValueBehavior: sets default values for method return and out arguments. In addition to the built-in supported default values, additional default value factories can be registered for any type.

    +
  • +
  • +

    DefaultEqualityBehavior: implements the Object.Equals and Object.GetHashCode members just like System.Object implements them.

    +
  • +
  • +

    RecordingBehavior: simple behavior that keeps track of all invocations, for troubleshooting or reporting.

    +
  • +
+ +

Customizing Avatar Creation

+ +

If you want to centrally configure all your avatars, the easiest way is to simply provide your own factory method (i.e. Stub.Of<T>), which in turn calls the Avatar.Of<T> provided. For example:

+ +
    public static class Stub
+    {
+        [AvatarGenerator]
+        public static T Of<T>() => Avatar.Of<T>()
+            .AddBehavior(new RecordingBehavior())
+            .AddBehavior(new DefaultEqualityBehavior())
+            .AddBehavior(new DefaultValueBehavior());
+    }
+
+ +

The [AvatarGenerator] attribute is required if you want to leverage the built-in compile-time code generation, since that signals to the source generator that calls to your API end up creating an avatar at run-time and therefore a generated type will be needed for it during compile-time. You can actually explore how this very same behavior is implemented in the built-in Avatar API which is provided as a content file:

+ +

avatar API source

+ +

The Avatar.cs contains, for example:

+ +
[AvatarGenerator]
+public static T Of<T>(params object[] constructorArgs) => Create<T>(constructorArgs);
+
+[AvatarGenerator]
+public static T Of<T, T1>(params object[] constructorArgs) => Create<T>(constructorArgs, typeof(T1));
+
+ +

As you can see, the Avatar API itself uses the same extensibility mechanism that your own custom factory methods can use.

+ +

Static vs Dynamic Avatars

+ +

Depending on the project and platform, Avatars will automatically choose whether to use run-time proxies or compile-time ones (powered by Roslyn source generators). The latter are only supported when building C# 9.0+ projects.

+ +

You can opt out of the static avatars by setting EnableCompiledAvatars=false in your project file:

+ +
<PropertyGroup>
+    <EnableCompiledAvatars>false</EnableCompiledAvatars>
+</PropertyGroup>
+
+ +

This will switch the project to run-time proxies based on Castle.Core.

+ +

Debugging Optimizations

+ +

There is nothing more frustrating than a proxy you have carefully configured that doesn’t behave the way you expect it to. In order to make this a less frustrating experience, avatars are carefully optimized for debugger display and inspection, so that it’s clear what behaviors are configured, and invocations and results are displayed clearly and concisely. Here’s the debugging display of the RecordingBehavior that just keeps track of invocations and their return values for example:

+ +

debugging display

+ +

And here’s the invocation debugger display from an anonymous behavior:

+ +

behavior debugging

+ +

Samples

+ +

The samples folder in the repository contains a few interesting examples of how Avatar can be used to implement some fancy use cases. For example:

+ +
    +
  • +

    Forwarding calls to matching interface methods/properties (by signature) to a static class. The example uses this to wrap calls to System.Console via an IConsole interface.

    +
  • +
  • +

    Forwarding calls to a target object using the DLR (that backs the dynamic keyword in C#) API for high-performance late binding.

    +
  • +
  • +

    Custom Stub.Of<T> factory that creates avatars that have common behaviors configured automatically.

    +
  • +
  • +

    Custom avatar factory method that adds an int return value randomizer.

    +
  • +
  • +

    Configuring the built-in DefaultValueBehavior so that every time a string property is retrieved, it gets a random lorem ipsum value.

    +
  • +
  • +

    Logging all calls to an avatar to the Xunit output helper.

    +
  • +
+ +

Sponsors

+ +

+sponsors  by @clarius sponsors +

+ +

get mentioned here too!

+ +
+
+ + + + + + + diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..83969dc --- /dev/null +++ b/license.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) Daniel Cazzulino and Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..a6bf9e8 --- /dev/null +++ b/readme.md @@ -0,0 +1,144 @@ +

Icon Avatar

+ +Avatar is a modern interception library which implements the [proxy pattern](https://en.wikipedia.org/wiki/Proxy_pattern) and runs everywhere, even where run-time code generation (Reflection.Emit) is forbidden or limitted, like physical iOS devices and game consoles, through compile-time code generation. The proxy behavior is configured in code using what we call a *behavior pipeline*. + +> *Avatars blend in with the Na'vi seamlessly, and you can control their behavior precisely by 'driving' them through a psionic link. Just like a [proxy](https://en.wikipedia.org/wiki/Proxy_pattern), with behavior driven through code.* + +![Avatar Overloads](https://github.com/kzu/avatar/raw/main/docs/images/AvatarIncubation.png) + +[![Version](https://img.shields.io/nuget/vpre/Avatar.svg?color=royalblue)](https://www.nuget.org/packages/Avatar) +[![Downloads](https://img.shields.io/nuget/dt/Avatar?color=darkmagenta)](https://www.nuget.org/packages/Avatar) +[![License](https://img.shields.io/github/license/devlooped/avatar.svg?color=blue)](https://github.com/devlooped/avatar/blob/main/LICENSE) +[![Discord Chat](https://img.shields.io/badge/chat-on%20discord-7289DA.svg)](https://discord.gg/AfGsdRa) +[![GitHub](https://img.shields.io/badge/-source-181717.svg?logo=GitHub)](https://github.com/devlooped/avatar) + +[![CI Version](https://img.shields.io/endpoint?url=https://shields.kzu.io/vpre/devlooped/main&label=nuget.ci&color=brightgreen)](https://pkg.kzu.io/index.json) +[![GH CI Status](https://github.com/kzu/avatar/workflows/build/badge.svg?branch=main)](https://github.com/devlooped/avatar/actions?query=branch%3Amain+workflow%3Abuild+) + + +> NOTE: Avatar provides a fairly low-level API with just the essential building blocks on top of which higher-level APIs can be built, such as the upcoming Moq vNext API. + +## Requirements + +Avatar is a .NET Standard 2.0 library and runs on any runtime that supports that. + +Compile-time proxy generation leverages [Roslyn source generators](https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.cookbook.md) and therefore requires C# 9.0, which at this time is included in Visual Studio 16.8 (preview or later) and the .NET 5.0 SDK (RC or later). Compile-time generated proxies support the broadest possible run-time platforms since they don't require any Reflection.Emit, and also don't pay that performance cost either. + +Whenever compile-time proxy generation is not available, a fallback generation strategy is used instead, which leverages [Castle DynamicProxy](https://github.com/castleproject/Core/blob/master/docs/dynamicproxy-introduction.md) to provide the run-time code generation. + +The client API for configuring proxy behaviors in either case is exactly the same. + +> NOTE: even though generated proxies is the main usage for Avatar, the API was designed so that you can also consume the behavior pipeline easily from hand-coded proxies too. + +## Usage + +```csharp +ICalculator calc = Avatar.Of(); + +calc.AddBehavior((invocation, next) => ...); +``` + +`AddBehavior`/`InsertBehavior` overloads allow granular control of the avatar's behavior pipeline, which is basically a [chain of responsibility](https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern) that invokes all configured behaviors that apply to the current invocation. Individual behaviors can determine whether to short-circuit the call or call the next behavior in the chain. + +![Avatar Overloads](https://github.com/kzu/avatar/raw/main/docs/images/AddInsertBehavior.png) + +Behaviors can also dynamically determine whether they apply to a given invocation by providing the optional `appliesTo` argument. In addition to the delegate-based overloads (called *anonymous behaviors*), you can also create behaviors by implementing the `IAvatarBehavior` interface: + +```csharp +public interface IAvatarBehavior +{ + bool AppliesTo(IMethodInvocation invocation); + IMethodReturn Execute(IMethodInvocation invocation, ExecuteHandler next); +} +``` + +## Common Behaviors + +Some commonly used behaviors that are generally useful are provided in the library and can be added to avatars as needed: + +* `DefaultValueBehavior`: sets default values for method return and *out* arguments. In addition to the built-in supported default values, additional default value factories can be registered for any type. + +* `DefaultEqualityBehavior`: implements the *Object.Equals* and *Object.GetHashCode* members just like *System.Object* implements them. + +* `RecordingBehavior`: simple behavior that keeps track of all invocations, for troubleshooting or reporting. + +## Customizing Avatar Creation + +If you want to centrally configure all your avatars, the easiest way is to simply provide your own factory method (i.e. `Stub.Of`), which in turn calls the `Avatar.Of` provided. For example: + +```csharp + public static class Stub + { + [AvatarGenerator] + public static T Of() => Avatar.Of() + .AddBehavior(new RecordingBehavior()) + .AddBehavior(new DefaultEqualityBehavior()) + .AddBehavior(new DefaultValueBehavior()); + } +``` + +The `[AvatarGenerator]` attribute is required if you want to leverage the built-in compile-time code generation, since that signals to the source generator that calls to your API end up creating an avatar at run-time and therefore a generated type will be needed for it during compile-time. You can actually explore how this very same behavior is implemented in the built-in Avatar API which is provided as a content file: + +![avatar API source](https://github.com/kzu/avatar/raw/main/docs/images/AvatarApi.png) + +The `Avatar.cs` contains, for example: + +```csharp +[AvatarGenerator] +public static T Of(params object[] constructorArgs) => Create(constructorArgs); + +[AvatarGenerator] +public static T Of(params object[] constructorArgs) => Create(constructorArgs, typeof(T1)); +``` + +As you can see, the Avatar API itself uses the same extensibility mechanism that your own custom factory methods can use. + +### Static vs Dynamic Avatars + +Depending on the project and platform, Avatars will automatically choose whether to use run-time proxies or compile-time ones (powered by Roslyn source generators). The latter are only supported when building C# 9.0+ projects. + +You can opt out of the static avatars by setting `EnableCompiledAvatars=false` in your project file: + +```xml + + false + +``` + +This will switch the project to run-time proxies based on Castle.Core. + +## Debugging Optimizations + +There is nothing more frustrating than a proxy you have carefully configured that doesn't behave the way you expect it to. In order to make this a less frustrating experience, avatars are carefully optimized for debugger display and inspection, so that it's clear what behaviors are configured, and invocations and results are displayed clearly and concisely. Here's the debugging display of the `RecordingBehavior` that just keeps track of invocations and their return values for example: + +![debugging display](https://github.com/kzu/avatar/raw/main/docs/images/DebuggerDisplay.png) + +And here's the invocation debugger display from an anonymous behavior: + +![behavior debugging](https://github.com/kzu/avatar/raw/main/docs/images/DebuggingBehavior.png) + +## Samples + +The `samples` folder in the repository contains a few interesting examples of how *Avatar* can be used to implement some fancy use cases. For example: + +* Forwarding calls to matching interface methods/properties (by signature) to a static class. The example uses this to wrap calls to *System.Console* via an *IConsole* interface. + +* Forwarding calls to a target object using the DLR (that backs the *dynamic* keyword in C#) API for high-performance late binding. + +* Custom `Stub.Of` factory that creates avatars that have common behaviors configured automatically. + +* Custom avatar factory method that adds an int return value randomizer. + +* Configuring the built-in *DefaultValueBehavior* so that every time a string property is retrieved, it gets a random lorem ipsum value. + +* Logging all calls to an avatar to the Xunit output helper. + + + +## Sponsors + +

+sponsors  by @clarius sponsors +

+ +*[get mentioned here too](https://github.com/sponsors/devlooped)!* diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props new file mode 100644 index 0000000..22b8b0c --- /dev/null +++ b/samples/Directory.Build.props @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/Directory.Build.targets b/samples/Directory.Build.targets new file mode 100644 index 0000000..0811cf3 --- /dev/null +++ b/samples/Directory.Build.targets @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/Samples/Core/Calculator.cs b/samples/Samples/Core/Calculator.cs new file mode 100644 index 0000000..daa1355 --- /dev/null +++ b/samples/Samples/Core/Calculator.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Samples +{ + class Calculator : ICalculator + { + readonly Dictionary memory = new(); + + public event EventHandler? TurnedOn; + public event EventHandler? TurnedOff; + + public int? this[string name] + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + public bool IsOn { get; private set; } + + public CalculatorMode Mode { get; set; } + + public int Add(int x, int y) => x + y; + + public int Add(int x, int y, int z) => x + y + z; + + public void Clear(string name) => memory.Remove(name); + + public int? Recall(string name) => memory.TryGetValue(name, out var value) ? value : null; + + public void Store(string name, int value) => memory[name] = value; + + public bool TryAdd(ref int x, ref int y, out int z) + { + z = x + y; + return true; + } + + public void TurnOn() + { + IsOn = true; + TurnedOn?.Invoke(this, EventArgs.Empty); + } + + public void TurnOff() + { + IsOn = false; + TurnedOff?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/samples/Samples/Core/CalculatorMode.cs b/samples/Samples/Core/CalculatorMode.cs new file mode 100644 index 0000000..8abdd85 --- /dev/null +++ b/samples/Samples/Core/CalculatorMode.cs @@ -0,0 +1,8 @@ +namespace Samples +{ + public enum CalculatorMode + { + Standard, + Scientific + } +} \ No newline at end of file diff --git a/samples/Samples/Core/ICalculator.cs b/samples/Samples/Core/ICalculator.cs new file mode 100644 index 0000000..aff09fc --- /dev/null +++ b/samples/Samples/Core/ICalculator.cs @@ -0,0 +1,33 @@ +using System; + +namespace Samples +{ + public interface ICalculator + { + event EventHandler? TurnedOn; + + event EventHandler? TurnedOff; + + bool IsOn { get; } + + CalculatorMode Mode { get; set; } + + int Add(int x, int y); + + int Add(int x, int y, int z); + + bool TryAdd(ref int x, ref int y, out int z); + + void TurnOn(); + + void TurnOff(); + + int? this[string name] { get; set; } + + void Store(string name, int value); + + int? Recall(string name); + + void Clear(string name); + } +} \ No newline at end of file diff --git a/samples/Samples/CustomGenerator.cs b/samples/Samples/CustomGenerator.cs new file mode 100644 index 0000000..3bed756 --- /dev/null +++ b/samples/Samples/CustomGenerator.cs @@ -0,0 +1,40 @@ +using System; +using System.Reflection; +using Avatars; +using Xunit; + +namespace Samples +{ + /// + /// Showcases how an arbitrary method can be annotated as a "avatar generator" that + /// will seamlessly extended default behaviors for new avatars. + /// + public class CustomGenerator + { + [Fact] + public void RandomPing() + { + var ping = Randomizer.Of(); + + // Each execution results in a new random value. + Assert.NotEqual(ping.Ping(), ping.Ping()); + Assert.NotEqual(ping.Ping(), ping.Ping()); + } + } + + public interface IPing + { + int Ping(); + } + + public static class Randomizer + { + static readonly Random random = new Random(); + + [AvatarGenerator] + public static T Of() + => Avatar.Of().AddBehavior( + (invocation, next) => invocation.CreateValueReturn(random.Next()), + invocation => invocation.MethodBase is MethodInfo info && info.ReturnType == typeof(int)); + } +} diff --git a/samples/Samples/Debugging.cs b/samples/Samples/Debugging.cs new file mode 100644 index 0000000..f063803 --- /dev/null +++ b/samples/Samples/Debugging.cs @@ -0,0 +1,41 @@ +using Avatars; +using Xunit; + +namespace Samples +{ + public class Debugging + { + /// + /// Set a breakpoint in the last line of the method and inspect the + /// avatar instance to see how Avatar optimizes and improves the + /// troubleshooting and inspection of running avatars and their + /// invocations. + /// + [Fact] + public void DebuggerDisplay() + { + var calc = Avatar.Of(); + var recorder = new RecordingBehavior(); + + calc.AddBehavior(recorder); + calc.AddBehavior(new DefaultEqualityBehavior()); + + calc.AddBehavior( + (invocation, next) => invocation.CreateValueReturn(invocation.Arguments.Get(0) + invocation.Arguments.Get(1)), + invocation => invocation.MethodBase.Name == nameof(ICalculator.Add), + "Add"); + + calc.AddBehavior(new DefaultValueBehavior()); + calc.GetHashCode(); + + Assert.False(calc.Equals(new object())); + Assert.Equal(5, calc.Add(3, 2)); + Assert.Equal(10, calc.Add(5, 5)); + + calc.Store("Mem1", 50); + calc.Recall("Mem1"); + + var calls = recorder.Invocations; + } + } +} diff --git a/samples/Samples/InterfaceStaticAdapter/InterfaceStaticAdapterBehavior.cs b/samples/Samples/InterfaceStaticAdapter/InterfaceStaticAdapterBehavior.cs new file mode 100644 index 0000000..1e88ad4 --- /dev/null +++ b/samples/Samples/InterfaceStaticAdapter/InterfaceStaticAdapterBehavior.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Avatars; + +namespace Samples +{ + /// + /// Forwards calls whose signature match to static methods on + /// another type. + /// + public class InterfaceStaticAdapterBehavior : IAvatarBehavior + { + readonly Type targetType; + readonly Dictionary targetMethods; + + public InterfaceStaticAdapterBehavior(Type targetType) + { + this.targetType = targetType; + targetMethods = targetType.GetMethods(BindingFlags.Public | BindingFlags.Static) + .ToDictionary(GetHashCode); + } + + public bool AppliesTo(IMethodInvocation invocation) => targetMethods.ContainsKey(GetHashCode(invocation.MethodBase as MethodInfo)); + + public IMethodReturn Execute(IMethodInvocation invocation, ExecuteHandler next) + { + var method = targetMethods[GetHashCode(invocation.MethodBase as MethodInfo)]; + var arguments = invocation.Arguments.ToArray(); + + try + { + var result = method.Invoke(null, arguments); + return invocation.CreateValueReturn(result, arguments); + } + catch (TargetInvocationException tie) + { + return invocation.CreateExceptionReturn(tie.InnerException!); + } + } + + int GetHashCode(MethodInfo? method) + { + if (method == null) + return 0; + + var hash = new HashCode(); + hash.Add(method.ReturnType); + hash.Add(method.Name); + + foreach (var type in method.GetGenericArguments()) + { + hash.Add(type); + } + + foreach (var parameter in method.GetParameters()) + { + hash.Add(parameter.ParameterType); + } + + return hash.ToHashCode(); + } + } +} diff --git a/samples/Samples/InterfaceStaticAdapter/InterfaceStaticAdapterTests.cs b/samples/Samples/InterfaceStaticAdapter/InterfaceStaticAdapterTests.cs new file mode 100644 index 0000000..fa239b7 --- /dev/null +++ b/samples/Samples/InterfaceStaticAdapter/InterfaceStaticAdapterTests.cs @@ -0,0 +1,47 @@ +using System; +using Avatars; + +namespace Samples +{ + /// + /// This example showcases how you can create a behavior that forwards + /// calls from an interface to matching methods in a static class, without + /// having to create an implementation for each such abstraction you want + /// to provide. + /// + public class InterfaceStaticAdapterTests + { + // Not annotated with Fact since that prevents console output collection. + // Use TestDriven.NET VS extension to run this test with the ad-hoc + // runner and see the actual forwarded calls to Console. + // [Fact] + public void Test() + { + var console = Avatar.Of() + .AddBehavior(new InterfaceStaticAdapterBehavior(typeof(Console))); + + console.Write("Hello"); + console.WriteLine(" World!"); + + console.ForegroundColor = ConsoleColor.Red; + console.WriteLine("Red!"); + console.BackgroundColor = ConsoleColor.Yellow; + console.WriteLine("Oh no, my eyes!"); + console.ResetColor(); + + // NOTE: if any of the above calls had not been properly forwarded, + // a NotImplementedException would have been thrown :). + } + } + + public interface IConsole + { + ConsoleColor BackgroundColor { get; set; } + ConsoleColor ForegroundColor { get; set; } + void ResetColor(); + + void Write(string value); + void WriteLine(string value); + } + +} diff --git a/samples/Samples/LoremIpsumDefaultValues.cs b/samples/Samples/LoremIpsumDefaultValues.cs new file mode 100644 index 0000000..c3e1100 --- /dev/null +++ b/samples/Samples/LoremIpsumDefaultValues.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avatars; +using Xunit; +using Xunit.Abstractions; + +namespace Samples +{ + /// + /// Showcases how to customize the DefaultValueBehavior so that each + /// time an avatar needs to return a string, a random lorem ipsum one + /// is returned. + /// + public class LoremIpsumDefaultValues + { + readonly ITestOutputHelper output; + + public LoremIpsumDefaultValues(ITestOutputHelper output) => this.output = output; + + [Fact] + public void LoremIpsumAvatar() + { + var provider = new DefaultValueProvider(); + provider.Register(typeof(string), _ => Ipsum.GetPhrase()); + + var greeter = Avatar.Of().AddBehavior(new DefaultValueBehavior(provider)); + + // On ever run, you'll get three different greetings! + output.WriteLine(greeter.Greet()); + output.WriteLine(greeter.Greet()); + output.WriteLine(greeter.Greet()); + } + } + + public interface IGreeter + { + string Greet(); + } + + /// + /// Mostly copied from https://github.com/netfx/extensions/blob/master/Extensions/Testing/IpsumGenerator/Source/Ipsum.cs + /// + static class Ipsum + { + static readonly Random random = new Random(); + static readonly string[] words = new string[] { "consetetur", "sadipscing", "elitr", "sed", "diam", "nonumy", "eirmod", +"tempor", "invidunt", "ut", "labore", "et", "dolore", "magna", "aliquyam", "erat", "sed", "diam", "voluptua", +"at", "vero", "eos", "et", "accusam", "et", "justo", "duo", "dolores", "et", "ea", "rebum", "stet", "clita", +"kasd", "gubergren", "no", "sea", "takimata", "sanctus", "est", "lorem", "ipsum", "dolor", "sit", "amet", +"lorem", "ipsum", "dolor", "sit", "amet", "consetetur", "sadipscing", "elitr", "sed", "diam", "nonumy", "eirmod", +"tempor", "invidunt", "ut", "labore", "et", "dolore", "magna", "aliquyam", "erat", "sed", "diam", "voluptua", +"at", "vero", "eos", "et", "accusam", "et", "justo", "duo", "dolores", "et", "ea", "rebum", "stet", "clita", +"kasd", "gubergren", "no", "sea", "takimata", "sanctus", "est", "lorem", "ipsum", "dolor", "sit", "amet", +"lorem", "ipsum", "dolor", "sit", "amet", "consetetur", "sadipscing", "elitr", "sed", "diam", "nonumy", "eirmod", +"tempor", "invidunt", "ut", "labore", "et", "dolore", "magna", "aliquyam", "erat", "sed", "diam", "voluptua", +"at", "vero", "eos", "et", "accusam", "et", "justo", "duo", "dolores", "et", "ea", "rebum", "stet", "clita", +"kasd", "gubergren", "no", "sea", "takimata", "sanctus", "est", "lorem", "ipsum", "dolor", "sit", "amet", "duis", +"autem", "vel", "eum", "iriure", "dolor", "in", "hendrerit", "in", "vulputate", "velit", "esse", "molestie", +"consequat", "vel", "illum", "dolore", "eu", "feugiat", "nulla", "facilisis", "at", "vero", "eros", "et", +"accumsan", "et", "iusto", "odio", "dignissim", "qui", "blandit", "praesent", "luptatum", "zzril", "delenit", +"augue", "duis", "dolore", "te", "feugait", "nulla", "facilisi", "lorem", "ipsum", "dolor", "sit", "amet", +"consectetuer", "adipiscing", "elit", "sed", "diam", "nonummy", "nibh", "euismod", "tincidunt", "ut", "laoreet", +"dolore", "magna", "aliquam", "erat", "volutpat", "ut", "wisi", "enim", "ad", "minim", "veniam", "quis", +"nostrud", "exerci", "tation", "ullamcorper", "suscipit", "lobortis", "nisl", "ut", "aliquip", "ex", "ea", +"commodo", "consequat", "duis", "autem", "vel", "eum", "iriure", "dolor", "in", "hendrerit", "in", "vulputate", +"velit", "esse", "molestie", "consequat", "vel", "illum", "dolore", "eu", "feugiat", "nulla", "facilisis", "at", +"vero", "eros", "et", "accumsan", "et", "iusto", "odio", "dignissim", "qui", "blandit", "praesent", "luptatum", +"zzril", "delenit", "augue", "duis", "dolore", "te", "feugait", "nulla", "facilisi", "nam", "liber", "tempor", +"cum", "soluta", "nobis", "eleifend", "option", "congue", "nihil", "imperdiet", "doming", "id", "quod", "mazim", +"placerat", "facer", "possim", "assum", "lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", +"elit", "sed", "diam", "nonummy", "nibh", "euismod", "tincidunt", "ut", "laoreet", "dolore", "magna", "aliquam", +"erat", "volutpat", "ut", "wisi", "enim", "ad", "minim", "veniam", "quis", "nostrud", "exerci", "tation", +"ullamcorper", "suscipit", "lobortis", "nisl", "ut", "aliquip", "ex", "ea", "commodo", "consequat", "duis", +"autem", "vel", "eum", "iriure", "dolor", "in", "hendrerit", "in", "vulputate", "velit", "esse", "molestie", +"consequat", "vel", "illum", "dolore", "eu", "feugiat", "nulla", "facilisis", "at", "vero", "eos", "et", "accusam", +"et", "justo", "duo", "dolores", "et", "ea", "rebum", "stet", "clita", "kasd", "gubergren", "no", "sea", +"takimata", "sanctus", "est", "lorem", "ipsum", "dolor", "sit", "amet", "lorem", "ipsum", "dolor", "sit", +"amet", "consetetur", "sadipscing", "elitr", "sed", "diam", "nonumy", "eirmod", "tempor", "invidunt", "ut", +"labore", "et", "dolore", "magna", "aliquyam", "erat", "sed", "diam", "voluptua", "at", "vero", "eos", "et", +"accusam", "et", "justo", "duo", "dolores", "et", "ea", "rebum", "stet", "clita", "kasd", "gubergren", "no", +"sea", "takimata", "sanctus", "est", "lorem", "ipsum", "dolor", "sit", "amet", "lorem", "ipsum", "dolor", "sit", +"amet", "consetetur", "sadipscing", "elitr", "at", "accusam", "aliquyam", "diam", "diam", "dolore", "dolores", +"duo", "eirmod", "eos", "erat", "et", "nonumy", "sed", "tempor", "et", "et", "invidunt", "justo", "labore", +"stet", "clita", "ea", "et", "gubergren", "kasd", "magna", "no", "rebum", "sanctus", "sea", "sed", "takimata", +"ut", "vero", "voluptua", "est", "lorem", "ipsum", "dolor", "sit", "amet", "lorem", "ipsum", "dolor", "sit", +"amet", "consetetur", "sadipscing", "elitr", "sed", "diam", "nonumy", "eirmod", "tempor", "invidunt", "ut", +"labore", "et", "dolore", "magna", "aliquyam", "erat", "consetetur", "sadipscing", "elitr", "sed", "diam", +"nonumy", "eirmod", "tempor", "invidunt", "ut", "labore", "et", "dolore", "magna", "aliquyam", "erat", "sed", +"diam", "voluptua", "at", "vero", "eos", "et", "accusam", "et", "justo", "duo", "dolores", "et", "ea", +"rebum", "stet", "clita", "kasd", "gubergren", "no", "sea", "takimata", "sanctus", "est", "lorem", "ipsum" }; + + /// + /// Gets a random Lorem Ipsum phrase with the given word count, starting with "Lorem ipsum dolor sit amet" + /// and ending with a dot. + /// + public static string GetPhrase(int? wordCount = null) + { + var count = wordCount ?? random.Next(50); + + var result = new List(); + result.Add("Lorem"); + result.Add("ipsum"); + result.Add("dolor"); + result.Add("sit"); + result.Add("amet"); + + for (var i = 5; i <= count; i++) + { + result.Add(words[random.Next(words.Length - 1)]); + } + + return string.Join(" ", result.Take(count)) + "."; + } + } +} diff --git a/samples/Samples/OutputHelperLoggingBehavior.cs b/samples/Samples/OutputHelperLoggingBehavior.cs new file mode 100644 index 0000000..0334892 --- /dev/null +++ b/samples/Samples/OutputHelperLoggingBehavior.cs @@ -0,0 +1,51 @@ +using Avatars; +using Xunit; +using Xunit.Abstractions; + +namespace Samples +{ + public class OutputHelperLoggingBehavior : IAvatarBehavior + { + readonly ITestOutputHelper output; + + public OutputHelperLoggingBehavior(ITestOutputHelper output) => this.output = output; + + public bool AppliesTo(IMethodInvocation invocation) => true; + + public IMethodReturn Execute(IMethodInvocation invocation, ExecuteHandler next) + { + var result = next.Invoke(invocation, next); + output.WriteLine(result.ToString()); + return result; + } + } + + public class OutputHelperLoggingTests + { + readonly ITestOutputHelper output; + + public OutputHelperLoggingTests(ITestOutputHelper output) => this.output = output; + + /// + /// Running this test will render all the calls in the output window + /// + //void TurnOn() + //int Add(int x: 3, int y: 5) => 0 + //void Store(string name: "m1", int value: 42) + //int? Recall(string name: "m1") => null + //void TurnOff() + [Fact] + public void LogsAllCalls() + { + var calc = Avatar.Of() + .AddBehavior(new OutputHelperLoggingBehavior(output)) + .AddBehavior(new DefaultValueBehavior()); + + calc.TurnOn(); + calc.Add(3, 5); + calc.Store("m1", 42); + calc.Recall("m1"); + calc.TurnOff(); + } + } +} diff --git a/samples/Samples/Samples.csproj b/samples/Samples/Samples.csproj new file mode 100644 index 0000000..88ff229 --- /dev/null +++ b/samples/Samples/Samples.csproj @@ -0,0 +1,18 @@ + + + + net472;net5.0 + enable + false + + + + + + + + + + + + diff --git a/samples/Samples/Stubs.cs b/samples/Samples/Stubs.cs new file mode 100644 index 0000000..dc98e08 --- /dev/null +++ b/samples/Samples/Stubs.cs @@ -0,0 +1,34 @@ +using System.Linq; +using Avatars; +using Xunit; + +namespace Samples +{ + public class Stubs + { + [Fact] + public void StubObjects() + { + var calculator = Stub.Of(); + + // Default behavior would return the default value for an int. + Assert.Equal(0, calculator.Add(2, 3)); + + // Use introspection API to explore configured behaviors + var avatar = (IAvatar)calculator; + var recorder = avatar.Behaviors.OfType().Single(); + + // Assert against the recorded invocations. + Assert.Single(recorder.Invocations); + } + } + + public static class Stub + { + [AvatarGenerator] + public static T Of() => Avatar.Of() + .AddBehavior(new DefaultEqualityBehavior()) + .AddBehavior(new RecordingBehavior()) + .AddBehavior(new DefaultValueBehavior()); + } +} diff --git a/samples/Samples/TargetInvocation/DynamicTargetBehavior.cs b/samples/Samples/TargetInvocation/DynamicTargetBehavior.cs new file mode 100644 index 0000000..bf93a26 --- /dev/null +++ b/samples/Samples/TargetInvocation/DynamicTargetBehavior.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using Avatars; +using Microsoft.CSharp.RuntimeBinder; + +namespace Samples.TargetInvocation +{ + public class DynamicTargetBehavior : IAvatarBehavior + { + readonly ConcurrentDictionary> invokers = new(); + readonly HashSet unsupported = new(); + readonly object target; + + public DynamicTargetBehavior(dynamic target) => this.target = target; + + public bool AppliesTo(IMethodInvocation invocation) + => !unsupported.Contains(invocation.MethodBase) && + // NOTE: doing the proper binding for ref/out arguments is quite complicated + // with the dynamic approach, since we would need to generate variables to keep the + // out/ref before/after the call. The generated IL from a dynamic call for that + // scenario is significantly more complicated than for "normal" method calls. + !invocation.MethodBase.GetParameters().Any(p => p.IsOut || p.ParameterType.IsByRef); + + public IMethodReturn Execute(IMethodInvocation invocation, ExecuteHandler next) + { + var invoker = invokers.GetOrAdd(invocation.MethodBase, method => GetInvoker(method)); + + try + { + return invoker(invocation); + } + catch (RuntimeBinderException) + { + unsupported.Add(invocation.MethodBase); + return next.Invoke(invocation, next); + } + } + + Func GetInvoker(MethodBase method) + { + var parameters = method.GetParameters(); + var binder = Microsoft.CSharp.RuntimeBinder.Binder.InvokeMember( + (method is MethodInfo mi && mi.ReturnType == typeof(void)) ? CSharpBinderFlags.ResultDiscarded : CSharpBinderFlags.None, + method.Name, + method.IsGenericMethod ? method.GetGenericArguments() : Array.Empty(), + GetType(), + new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }.Concat( + method.GetParameters().Select(p => CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, p.Name)))); + + if (method is MethodInfo info && info.ReturnType == typeof(void)) + { + var delegateType = Type.GetType("System.Action`" + (parameters.Length + 2), true)! + .MakeGenericType( + new[] { typeof(CallSite), typeof(object) } + .Concat(parameters.Select(p => p.ParameterType)) + .ToArray()); + + var site = typeof(CallSite<>).MakeGenericType(delegateType) + .InvokeMember("Create", BindingFlags.Static | BindingFlags.Public | BindingFlags.InvokeMethod, null, null, new[] { binder }); + + var target = (Delegate)site?.GetType().GetField("Target")?.GetValue(site)!; + + return invocation => + { + var args = invocation.Arguments.Select(x => x.RawValue).ToArray(); + try + { + target.DynamicInvoke(new[] { site, this.target }.Concat(args).ToArray()); + return invocation.CreateReturn(); + } + catch (TargetInvocationException tie) + { + return invocation.CreateExceptionReturn(tie.InnerException!); + } + }; + } + else + { + var delegateType = Type.GetType("System.Func`" + (parameters.Length + 3), true)! + .MakeGenericType( + new[] { typeof(CallSite), typeof(object) } + .Concat(parameters.Select(p => p.ParameterType)) + .Concat(new[] { typeof(object) }) + .ToArray()); + + var site = typeof(CallSite<>).MakeGenericType(delegateType) + .InvokeMember("Create", BindingFlags.Static | BindingFlags.Public | BindingFlags.InvokeMethod, null, null, new[] { binder }); + + var target = (Delegate)site?.GetType().GetField("Target")?.GetValue(site)!; + + return invocation => + { + var args = invocation.Arguments.Select(x => x.RawValue).ToArray(); + try + { + var result = target.DynamicInvoke(new[] { site, this.target }.Concat(args).ToArray()); + return invocation.CreateValueReturn(result); + } + catch (TargetInvocationException tie) + { + return invocation.CreateExceptionReturn(tie.InnerException!); + } + }; + } + } + } +} diff --git a/samples/Samples/TargetInvocation/DynamicTargetTests.cs b/samples/Samples/TargetInvocation/DynamicTargetTests.cs new file mode 100644 index 0000000..a0eab8e --- /dev/null +++ b/samples/Samples/TargetInvocation/DynamicTargetTests.cs @@ -0,0 +1,51 @@ +using Avatars; +using Xunit; + +namespace Samples.TargetInvocation +{ + public class DynamicTargetTests + { + [Fact] + public void InvokeReturningTarget() + { + var target = new Calculator(); + + ICalculator calc = Avatar.Of(); + var recorder = new RecordingBehavior(); + + // By adding the recorder *after* the dynamic target, + // we can check if any calls where made to the avatar + // instead of the target. + calc.AddBehavior(new DynamicTargetBehavior(target)) + .AddBehavior(recorder); + + var result = calc.Add(2, 3); + + // We recorded the call to the avatar, but the + // dynamic target behavior passed the call through + // to the real calculator which did the math. + Assert.Empty(recorder.Invocations); + Assert.Equal(5, result); + } + + [Fact] + public void InvokeVoidTarget() + { + var target = new Calculator(); + var avatar = Avatar.Of(); + var avatarRecorder = new RecordingBehavior(); + + // By adding the recorder *after* the dynamic target, + // we can check if any calls where made to the avatar + // instead of the target. + avatar.AddBehavior(new DynamicTargetBehavior(target)) + .AddBehavior(avatarRecorder); + + avatar.Store("m1", 42); + Assert.Equal(42, avatar.Recall("m1")); + + avatar.Clear("m1"); + Assert.Null(avatar.Recall("m1")); + } + } +}