diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..0ba8230 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,43 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["master"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: '.' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72a5c3b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +_site/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1a481de --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "githubPullRequests.ignoredPullRequestBranches": [ + "master", + "master" + ] +} diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..e43c111 --- /dev/null +++ b/Gemfile @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +# gem "rails" + +gem "jekyll", "~> 4.4" + +gem "jekyll-redirect-from", "~> 0.16.0" + +gem "jekyll-last-modified-at", "~> 1.3" + +gem "activesupport", "~> 8.0" + +gem "jekyll-feed", "~> 0.17.0" + +# Avoid polling for changes on Windows +gem 'wdm', '>= 0.1.0' if Gem.win_platform? +gem "jekyll-toc", "~> 0.19.0" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..91b8d26 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,121 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (8.0.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + base64 (0.2.0) + benchmark (0.4.0) + bigdecimal (3.1.9) + colorator (1.1.0) + concurrent-ruby (1.3.5) + connection_pool (2.5.0) + csv (3.3.2) + drb (2.2.1) + em-websocket (0.5.3) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0) + eventmachine (1.2.7) + ffi (1.17.1-x64-mingw-ucrt) + forwardable-extended (2.6.0) + google-protobuf (4.29.3-x64-mingw-ucrt) + bigdecimal + rake (>= 13) + http_parser.rb (0.8.0) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + jekyll (4.4.1) + addressable (~> 2.4) + base64 (~> 0.2) + colorator (~> 1.0) + csv (~> 3.0) + em-websocket (~> 0.5) + i18n (~> 1.0) + jekyll-sass-converter (>= 2.0, < 4.0) + jekyll-watch (~> 2.0) + json (~> 2.6) + kramdown (~> 2.3, >= 2.3.1) + kramdown-parser-gfm (~> 1.0) + liquid (~> 4.0) + mercenary (~> 0.3, >= 0.3.6) + pathutil (~> 0.9) + rouge (>= 3.0, < 5.0) + safe_yaml (~> 1.0) + terminal-table (>= 1.8, < 4.0) + webrick (~> 1.7) + jekyll-feed (0.17.0) + jekyll (>= 3.7, < 5.0) + jekyll-last-modified-at (1.3.2) + jekyll (>= 3.7, < 5.0) + jekyll-redirect-from (0.16.0) + jekyll (>= 3.3, < 5.0) + jekyll-sass-converter (3.1.0) + sass-embedded (~> 1.75) + jekyll-toc (0.19.0) + jekyll (>= 3.9) + nokogiri (~> 1.12) + jekyll-watch (2.2.1) + listen (~> 3.0) + json (2.10.1) + kramdown (2.5.1) + rexml (>= 3.3.9) + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.4) + listen (3.9.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + logger (1.6.6) + mercenary (0.4.0) + minitest (5.25.4) + nokogiri (1.18.2-x64-mingw-ucrt) + racc (~> 1.4) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (6.0.1) + racc (1.8.1) + rake (13.2.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rexml (3.4.0) + rouge (4.5.1) + safe_yaml (1.0.5) + sass-embedded (1.83.4-x64-mingw-ucrt) + google-protobuf (~> 4.29) + securerandom (0.4.1) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (2.6.0) + uri (1.0.2) + wdm (0.2.0) + webrick (1.9.1) + +PLATFORMS + x64-mingw-ucrt + +DEPENDENCIES + activesupport (~> 8.0) + jekyll (~> 4.4) + jekyll-feed (~> 0.17.0) + jekyll-last-modified-at (~> 1.3) + jekyll-redirect-from (~> 0.16.0) + jekyll-toc (~> 0.19.0) + wdm (>= 0.1.0) + +BUNDLED WITH + 2.5.22 diff --git a/README.md b/README.md index 4e1306d..8121339 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ -These are email chains used by PostSharp Technologies. +This repo contains the source and images of the chains used by PostSharp Technologies. + +The publishing process is the following: + +- There is a GitHub action that publishes all static files to https://emails.postsharp.net/. The reason for this is to publish _images_. +- Run Jekyll (`run-all.ps1`) to generate all HTML emails from Markdown. +- In ConvertKit: + - Create an HTML field and paste the raw HTML. + + diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..2d38e01 --- /dev/null +++ b/_config.yml @@ -0,0 +1,45 @@ +# Welcome to Jekyll! +# +# This config file is meant for settings that affect your whole blog, values +# which you are expected to set up once and rarely need to edit after that. +# For technical reasons, this file is *NOT* reloaded automatically when you use +# 'jekyll serve'. If you change this file, please restart the server process. + +# Site settings +title: Metalama +email: hello@postsharp.net +base_path: "" # the subpath of your site, e.g. /blog +url: "https://metalama.net" # the base hostname & protocol for your site + + +strict_front_matter: true + +title_separator: "|" + + +exclude: ["*.ps1", "README.md", "package.json", "package-lock.json", "gulpfile.js", "eng/**"] +include: ["solutions/*.md"] + +sass: + style: compressed + sourcemap: always + silence_deprecations: ["import", "slash-div"] + + +defaults: + - scope: + path: "" + type: "pages" + values: + layout: "email" + - scope: + path: "metalama-email-course" + values: + images_url: "https://emails.postsharp.net/metalama-email-course/images" + +markdown: kramdown +kramdown: + highlighter: rouge + syntax_highlighter: rouge + syntax_highlighter_opts: + line_numbers: false diff --git a/_layouts/email.html b/_layouts/email.html new file mode 100644 index 0000000..f67fb92 --- /dev/null +++ b/_layouts/email.html @@ -0,0 +1,333 @@ + +Fellow code quality warrior, greetings! +{{ content }} +

Need Help or Want to Learn More?

+

+ Browse our documentationand commented examples to explore + more. Got a question? Post it on GitHub + discussions. +

+

+ Happy meta-programming! +

\ No newline at end of file diff --git a/_plugins/absolute_urls.rb b/_plugins/absolute_urls.rb new file mode 100644 index 0000000..ed3516c --- /dev/null +++ b/_plugins/absolute_urls.rb @@ -0,0 +1,16 @@ +Jekyll::Hooks.register [:pages, :documents], :post_render do |page| + images_url = page.data['images_url'] + + # Only modify HTML output + if page.output_ext == '.html' + + if images_url + + # Change path to images. + page.output.gsub!(%r{(]*src=["'])images([^"']*)(["'])}) do + "#{$1}#{images_url}#{$2}#{$3}" + end + + end + end +end diff --git a/_plugins/embedded.rb b/_plugins/embedded.rb new file mode 100644 index 0000000..c93d89c --- /dev/null +++ b/_plugins/embedded.rb @@ -0,0 +1,37 @@ +class EmbeddedTag < Liquid::Tag + def initialize(tag_name, markup, tokens) + super + + @attributes = {} + markup.scan(::Liquid::TagAttributes) do |key, value| + @attributes[key] = value + end + @markup = markup + + end + + def render(context) + + id = @attributes['id'] + url = @attributes['url'] + node = @attributes['node'] + + + # Write the output HTML string + + output = "
" + output += " " + + # Render it on the page by returning it + return output; + + end + +end + +Liquid::Template.register_tag('embedded', EmbeddedTag) \ No newline at end of file diff --git a/_plugins/jekyll_include_plugin.rb b/_plugins/jekyll_include_plugin.rb new file mode 100644 index 0000000..2e43eda --- /dev/null +++ b/_plugins/jekyll_include_plugin.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require_relative "jekyll_include_plugin/version" +require_relative "jekyll_include_plugin/utils" +require_relative "jekyll_include_plugin/jekyll_include_plugin" + +Liquid::Template.register_tag("include_file", JekyllIncludePlugin::IncludeFileTag) diff --git a/_plugins/jekyll_include_plugin/jekyll_include_plugin.rb b/_plugins/jekyll_include_plugin/jekyll_include_plugin.rb new file mode 100644 index 0000000..cc5e4e7 --- /dev/null +++ b/_plugins/jekyll_include_plugin/jekyll_include_plugin.rb @@ -0,0 +1,147 @@ +# TODO: don't read the whole file into the memory from the beginning, instead process file with the parser line by line +require "open-uri" +require "liquid" +require 'active_support/all' +require_relative "utils" + +class CachedFetcher + # Initialize a class-level instance variable for the cache + @cache = ActiveSupport::Cache::MemoryStore.new + + # Class method to fetch data with caching + def self.fetch(url) + # Access the class-level instance variable using self.class + @cache.fetch(url, expires_in: 5.minutes) do + URI.open(url).read # This is executed only if the cache is missed + end + end +end + + +module JekyllIncludePlugin + class IncludeFileTag < Liquid::Tag + include Utils + include TextUtils + include GitHubUtils + + def initialize(tag_name, raw_markup, tokens) + super + @raw_markup = raw_markup + @params = {} + end + + def render(context) + parse_params(context) + + file_contents = get_raw_file_contents(context) + + if @params["snippet"] + file_contents = pick_snippet(file_contents, @params["snippet"], context) + file_contents = remove_excessive_indentation(file_contents) + else + file_contents = remove_header(file_contents) + file_contents = remove_all_snippets(file_contents) + end + + file_contents = file_contents.strip + + file_contents = render_comments(file_contents, context.registers[:page]["lang"]) + file_contents = add_original_indentation( file_contents ) + + file_contents = wrap_in_codeblock(file_contents, @params["syntax"]) if @params["syntax"] + + + return file_contents + end + + private + + def parse_params(context) + rendered_markup = Liquid::Template + .parse(@raw_markup) + .render(context) + .gsub(%r!\\\{\\\{|\\\{\\%!, '\{\{' => "{{", '\{\%' => "{%") + .strip + debug("Rendered params: #{rendered_markup}") + + markup = %r!^"?(?[^\s\"]+)"?(?(\s+\w+="[^\"]+")*)?$!.match(rendered_markup) + debug("Matched params: #{markup.inspect}") + error("Can't parse include_file tag params: #{@raw_markup}", context) unless markup + + if markup[:params] + @params = Hash[ *markup[:params].scan(%r!(?[^\s="]+)(?:="(?[^"]+)")?\s?!).flatten ] + end + + if %r!^https?://\S+$!.match?(markup[:path]) + @params["abs_file_url"] = markup[:path] + elsif %r!^file?://\S+$!.match?(markup[:path]) + @params["abs_file_url"] = markup[:path].sub(%r!^file://!, '').gsub('\\', '/') + else + @params["rel_file_path"] = markup[:path] + end + + validate_param_by_regex("snippet", "^[-_.a-zA-Z0-9]+$", context) + validate_param_by_regex("syntax", "^[-_.a-zA-Z0-9]+$", context) + + debug("Params set: #{@params.inspect}") + end + + def validate_param_by_regex(param_name, param_regex, context) + if @params[param_name] && ! %r!#{param_regex}!.match?(@params[param_name]) + error("Parameter '#{param_name}' with value '#{@params[param_name]}' is not valid, must match regex: #{param_regex}", context) + end + end + + def get_raw_file_contents(context) + if @params["abs_file_url"] + return get_remote_file_contents(context) + elsif @params["rel_file_path"] + return get_local_file_contents(context) + end + raise "Neither abs_file_url nor rel_file_path have been found" + end + + def get_local_file_contents(context) + base_source_dir = File.expand_path(context.registers[:site].config["source"]).freeze + abs_file_path = File.join(base_source_dir, @params["rel_file_path"]) + + begin + debug("Getting contents of specified local file: #{abs_file_path}") + return File.read(abs_file_path, **context.registers[:site].file_read_opts) + rescue SystemCallError, IOError => e + return error("Can't get the contents of specified local file '#{abs_file_path}' (for '#{@params["rel_file_path"]}'): #{e.message}", context) + end + end + + def get_remote_file_contents(context) + begin + url = convert_github_url_to_raw(@params["abs_file_url"]) + debug("Getting contents of specified remote file: #{url}") + + if url !~ /^https?:\/\// + return URI.open(url).read + else + return CachedFetcher.fetch(url) + end + rescue => e + return error("An error occurred while fetching the file '#{url}': #{e.message}", context) + end + end + + def add_original_indentation(text) + # Determine the indentation level by checking the first non-empty line + indentation_level = n = @params["indent"].to_i + indentation = ' ' * indentation_level + + # Split the content into lines + lines = text.split("\n") + + # Don't indent the first line, but indent subsequent lines + lines[1..] = lines[1..].map { |line| "#{indentation}#{line}" } + + # Join the lines back together and return the result + lines.join("\n") + end + + end +end diff --git a/_plugins/jekyll_include_plugin/utils.rb b/_plugins/jekyll_include_plugin/utils.rb new file mode 100644 index 0000000..1bfabbf --- /dev/null +++ b/_plugins/jekyll_include_plugin/utils.rb @@ -0,0 +1,191 @@ +module JekyllIncludePlugin + module Utils + def debug(msg) + Jekyll.logger.debug("[jekyll_include_plugin] DEBUG:", msg) + end + + def info(msg) + Jekyll.logger.info("[jekyll_include_plugin] INFO:", msg) + end + + def abort(msg) + Jekyll.logger.abort_with("[jekyll_include_plugin] FATAL:", msg) + end + + def error(msg, context) + page = context.registers[:page]; + Jekyll.logger.error("[jekyll_include_plugin] ERROR:", "#{msg} in #{page['path']}") + return "ERROR: #{msg} in #{page['path']}" + end + end + + module GitHubUtils + def convert_github_url_to_raw(url) + # Check if the URL is a GitHub URL + if url.start_with?('https://github.com/') + # Replace 'https://github.com/' with 'https://raw.githubusercontent.com/' + # Change 'tree/main/' to 'main/' + url.sub('https://github.com/', 'https://raw.githubusercontent.com/') + .sub('/tree/', '/') + else + # Return the original URL if it's not a GitHub URL + url + end + end + end + + module TextUtils + include Utils + + def pick_snippet(text, snippet_name, context) + snippet_content = "" + snippet_start_found = false + snippet_end_found = false + text.each_line do |line| + if %r!\[\]!.match?(line) + if snippet_start_found + return error("Snippet '#{snippet_name}' occured twice. Each snippet should have a unique name, same name not allowed.", context) + end + snippet_start_found = true + debug("Snippet '#{snippet_name}' start matched by line: #{line}") + elsif %r!\[\]!.match?(line) + snippet_end_found = true + debug("Snippet '#{snippet_name}' end matched by line: #{line}") + break + elsif %r!\[<(end)?snippet\s+[^>]+>\]!.match?(line) + debug("Skipping line with non-relevant (end)snippet: #{line}") + next + elsif snippet_start_found + snippet_content += line + end + + + end + + unless snippet_start_found + return error( "Snippet '#{snippet_name}' has not been found in '#{@params["abs_file_url"]}#{@params["rel_file_url"]}'.", context) + end + + unless snippet_end_found + return error("End of the snippet '#{snippet_name}' has not been found.", context) + end + + if snippet_content.empty? + return error("Snippet '#{snippet_name}' appears to be empty. Fix and retry.", context) + end + + + first_line_indent = %r!^\s*!.match(snippet_content)[0] + return "#{first_line_indent}\n#{snippet_content}" + end + + def remove_all_snippets(text) + result_text = "" + text.each_line do |line| + if %r!\[<(end)?snippet\s+[^>]+>\]!.match?(line) + debug("Skipping line with non-relevant (end)snippet: #{line}") + next + else + result_text += line + end + end + + return result_text + end + + def render_comments(text, lang) + rendered_file_contents = "" + text.each_line do |line| + if %r!\[<#{lang}>\]!.match?(line) + debug("Matched doc line: #{line}") + rendered_file_contents += line.sub(/\[<#{lang}>\]\s*/, "") + elsif %r!\[<\w+>\]!.match?(line) + debug("Found non-matching doc line, skipping: #{line}") + next + else + rendered_file_contents += line + end + end + + return rendered_file_contents + end + + def remove_excessive_indentation(text) + unindented_text = "" + + lowest_indent = nil + text.each_line do |line| + if %r!^\s*$!.match?(line) + next + else + cur_indent = %r!^\s*!.match(line)[0].length + lowest_indent = cur_indent if lowest_indent.nil? || lowest_indent > cur_indent + end + end + return text if lowest_indent.nil? + + text.each_line do |line| + if blank_line?(line) + unindented_text += line + else + unindented_text += line[lowest_indent..-1] + end + end + + return unindented_text + end + + def wrap_in_codeblock(text, syntax) + return "```#{syntax}\n#{text}\n```" + end + + def blank_line?(line) + return %r!^\s*$!.match?(line) + end + + def remove_any_bom(input_string) + # Define the possible BOMs + boms = { + 'UTF-8' => "\xEF\xBB\xBF", + 'UTF-16BE' => "\xFE\xFF", + 'UTF-16LE' => "\xFF\xFE", + 'UTF-32BE' => "\x00\x00\xFE\xFF", + 'UTF-32LE' => "\xFF\xFE\x00\x00" + } + + # Check if the input string starts with any BOM and remove it + boms.each_value do |bom| + if input_string.start_with?(bom) + input_string = input_string[bom.length..-1] + break + end + end + + # Return the cleaned string + input_string + end + + def remove_header(text) + + text = remove_any_bom( text ) + filtered_text = "" + # Variable to track if the previous line was removed + prev_removed = false + + # Iterate over each line in the input text + text.each_line do |line| + stripped_line = line.strip + if stripped_line.start_with?('// Copyright') || %r!^using [\w.]+;!.match?(stripped_line) || %r!^namespace [\w.]+;!.match?(stripped_line) || stripped_line.start_with?('#') + prev_removed = true + elsif prev_removed && line.strip.empty? + # remove this one too. + else + filtered_text += line + prev_removed = false + end + end + + return filtered_text + end + end +end diff --git a/_plugins/jekyll_include_plugin/version.rb b/_plugins/jekyll_include_plugin/version.rb new file mode 100644 index 0000000..25c0ddd --- /dev/null +++ b/_plugins/jekyll_include_plugin/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module JekyllIncludePlugin + VERSION = "1.1.1" +end diff --git a/_plugins/warn.rb b/_plugins/warn.rb new file mode 100644 index 0000000..e8c9c97 --- /dev/null +++ b/_plugins/warn.rb @@ -0,0 +1,24 @@ +module Jekyll + module WarnTag + class Tag < Liquid::Tag + def initialize(tag_name, text, tokens) + super + @text = text.strip + end + + def render(context) + # Access the current file path from the context + current_file = context.registers[:page]["path"] + + # Log the warning with the file path + Jekyll.logger.warn "Liquid Warning in #{current_file}:", @text + + # Return an empty string to prevent rendering the tag content + "" + end + end + end + end + + Liquid::Template.register_tag('warn', Jekyll::WarnTag::Tag) + \ No newline at end of file diff --git a/metalama-awareness-campaign/01_more_effective_way.md b/metalama-awareness-campaign/01_more_effective_way.md new file mode 100644 index 0000000..8aa4a93 --- /dev/null +++ b/metalama-awareness-campaign/01_more_effective_way.md @@ -0,0 +1,74 @@ +--- +--- +# Discover Metalama: A New Code Generation Toolkit for C# + +Hi {{firstName}}, + +My name is **{{sendingAccountFirstName}}** and I’m helping PostSharp’s founder to gather feedback about **Metalama**, a new, free code generation and validation toolkit for C#. + +Given your experience in .NET, I thought you might be interested to learn about it and, on our side, we’re eager to hear your opinion, therefore this cold email. + +The idea behind Metalama is that you write custom attributes called **aspects**, which work as code templates. Here is our Hello World example: a logging aspect. + +```csharp +using Metalama.Framework.Aspects; + +public class LogAttribute : OverrideMethodAspect +{ +  public override dynamic? OverrideMethod() +  { +    Console.WriteLine( "Entering {meta.Target.Method}." ); +    try +    { +       return meta.Proceed(); +    } +    finally +    { +       Console.WriteLine( "Leaving {meta.Target.Method}." ); +    } +  } +} +``` + +You can then use the aspect on any method, like this: + +```csharp +[Log] +void SomeMethod() => Console.WriteLine( "Hello, World!" ); +``` + +Metalama transforms the code during compilation into this: + +```csharp +void SomeMethod() +{ +  Console.WriteLine( "Entering Program.SomeMethod()" ); +  try +  { +    Console.WriteLine( "Hello, World!" ); +  } +  finally +  { +  Console.WriteLine( "Leaving Program.SomeMethod()" ); +  } +} +``` + +It’s possible to preview or even debug the code generated by Metalama. The real benefit of this approach is that you can **drastically reduce repetitive code**. And if you want to change the code generation pattern, you just need to change the aspect. You get the idea! + +**Key Benefits of Metalama:** + +- **Reduce repetitive code** +- **Easy code modifications** +- **Real-time feedback and code modifications** +- **Preview and debug generated code** + +You can find the source code of this example, as well as more examples, on [GitHub](https://github.com/postsharp/Metalama.Demo/blob/master/src/01_Log/LogAttribute.cs?mtm_campaign=awareness&mtm_source=instantly). You can learn more about Metalama thanks to the [video tutorials](https://doc.metalama.net/videos?mtm_campaign=awareness&mtm_source=instantly) and [commented examples](https://doc.metalama.net/examples?mtm_campaign=awareness&mtm_source=instantly). + +Our development team is looking forward to your feedback and questions on our [Slack community workspace](https://www.postsharp.net/slack?mtm_campaign=awareness&mtm_source=instantly). Of course, you can also answer this email and I’ll make sure it will reach an engineer. + +Thank you! + +All the best, +**{{sendingAccountFirstName}}** +Community Manager diff --git a/metalama-awareness-campaign/02_naming_conventions.md b/metalama-awareness-campaign/02_naming_conventions.md new file mode 100644 index 0000000..bcdce1c --- /dev/null +++ b/metalama-awareness-campaign/02_naming_conventions.md @@ -0,0 +1,38 @@ +# Validate Naming Conventions with Metalama + +Hi {{firstName}}, + +In my previous email I briefly showed how you can use Metalama to generate code. Today, I would like to introduce Metalama’s second pillar: code verification. + +You’ve perhaps experienced how hard it can be to align everyone on the same naming conventions. With Metalama, you define rules and conventions using plain C#. They will be enforced both in real-time in the IDE and at compile time. + +For instance, assume you want every class implementing IInvoiceFactory to have the InvoiceFactory suffix. You can do this with a single attribute. + +```csharp +[DerivedTypesMustRespectNamingConvention( "*InvoiceFactory" )] +public interface IInvoiceFactory +{ +Invoice CreateFromOrder( Order order ); +} +``` + +If someone violates this rule, a warning will immediately be reported: + +``` +LAMA0903. The type ‘MyInvoiceConverted’ does not respect the naming convention set on the base class or interface ‘IInvoiceFactory’. The type name should match the "\*InvoiceFactory" pattern. +``` + +The shorter the feedback loop is, the smoother the code reviews will go! Not talking about the frustration both sides avoided! + +You can learn more about code validation with Metalama in our [online documentation](https://doc.metalama.net/examples?mtm_campaign=awareness&mtm_source=instantly). + +As I wrote in my first email, our development team is looking forward to your feedback and questions on our [Slack community workspace](https://www.postsharp.net/slack?mtm_campaign=awareness&mtm_source=instantly). Of course, you can also answer this email and I’ll make sure it will reach an engineer. + +Thank you! + +All the best, +**{{sendingAccountFirstName}}** +Community Manager + +_P.S. We will send you 3 more emails about Metalama and then stop. You can unsubscribe at any time._ + diff --git a/metalama-awareness-campaign/03_caching.md b/metalama-awareness-campaign/03_caching.md new file mode 100644 index 0000000..a464958 --- /dev/null +++ b/metalama-awareness-campaign/03_caching.md @@ -0,0 +1,99 @@ +# Here is How to Save Time on Caching + + +Hello! + +It's Fedja from Metalama again. In my previous emails, I described Metalama as a meta-programming _framework_ that allows you to generate and validate code as you type. + +If you looked at our API and documentation, you may have got the impression that the framework is deep and complex. + +Good news is, **you don't need to write your own aspects**. You can find open-source and professionally supported aspects on [Metalama Marketplace](https://www.postsharp.net/metalama/marketplace), including code contracts, caching, observability (INotifyPropertyChanged), WPF commands and dependency properties, and much more. + +Let's look at caching today. + +## Adding Caching to Your App + +Metalama supports caching with and without Dependency Injection (DI). In our first example, we will explore its usage in a project that employs DI. + +You can add caching to your app in just three steps: + +1. Add the [Metalama.Patterns.Caching.Aspects](https://www.nuget.org/packages/Metalama.Patterns.Caching.Aspects/) package to your project. +2. Navigate to all methods that need caching and add the `[Cache]` custom attribute. + + + ```c# + using Metalama.Patterns.Caching.Aspects; + + namespace CreatingAspects.Caching + { + public sealed class CloudCalculator + { + + [Cache] + public int Add(int a, int b) + { + Console.WriteLine("Doing some very hard work."); + + this.OperationCount++; + + Console.WriteLine("Finished doing some very hard work."); + + return a + b; + } + + public int OperationCount { get; private set; } + } + } + ``` + +3. Go to the application startup code and call `AddMetalamaCaching`. This adds the `ICachingService` interface to your `IServiceCollection`, enabling the `[Cache]` aspect to be used on all objects instantiated by the DI container. + + ```c# + builder.Services.AddMetalamaCaching(); + ``` + + This will use the `MemoryCache` by default. If you want to [use Redis](https://doc.metalama.net/preview/patterns/caching/redis), do this: + + ```c# + builder.Services.AddMetalamaCaching( caching => caching.WithBackend( backend => backend.Redis() ) ); + ``` + + Want a `MemoryCache` _in front_ of your Redis cache? No problem. The service will listen to Redis notifications to invalidate the L1 cache. + + ```c# + builder.Services.AddMetalamaCaching( + caching => caching.WithBackend( + backend => backend.Redis().WithL1() ) ); + ``` + +## What Metalama does for you + +At build time, Metalama transforms your code on-the-fly: +* It pulls the dependency to `ICachingService`. +* It generates the [cache key](https://doc.metalama.net/preview/patterns/caching/caching-keys). +* It wraps your cached method into a delegate before calling `ICachingService.GetOrAdd`. + +Other features of this open-source caching library include: + +* Serialization. +* Robust [invalidation](https://doc.metalama.net/preview/patterns/caching/invalidation). Say goodbye to the cache key hell. +* Multi-node [synchronization](https://doc.metalama.net/preview/patterns/caching/pubsub) over Redis or Azure Service Bus. +* Transparent handling of weird types like `IEnumerable` or `Stream`. +* [Locking](https://doc.metalama.net/preview/patterns/caching/locking). +* Compatible with .NET Aspire. + +## Conclusion + +Applying caching to an application can dramatically improve performance, but implementing the pattern by hand is not straightforward. Luckily, Metalama does all the heavy lifting for you while remaining flexible enough so you can customize the library to meet your specific requirements. + +Caching is just one of the open-source aspects built by our team. For more, check [Metalama Marketplace](https://www.postsharp.net/metalama/marketplace). + +Our development team is looking forward to your feedback and questions on our [Slack community workspace](https://www.postsharp.net/slack). Of course, you can also answer this email and I’ll make sure it will reach an engineer. + +Thank you! + +All the best, +Fedja +Community Manager + +*P.S. We will send you 2 more emails about Metalama and then stop. You can unsubscribe at any time.* \ No newline at end of file diff --git a/metalama-awareness-campaign/04_validating_architecture.md b/metalama-awareness-campaign/04_validating_architecture.md new file mode 100644 index 0000000..ba981a5 --- /dev/null +++ b/metalama-awareness-campaign/04_validating_architecture.md @@ -0,0 +1,83 @@ +# Get your development team to adhere to architecture + +Developers need to follow specific rules and conventions to work together effectively as a team. This adherence ensures that individual contributions integrate seamlessly into the overall application. + +In a previous email, we discussed how to enforce naming conventions. Today, let's examine how to verify that components are _used_ as expected. You might have just written a method meant only to support a test, but how can you enforce that it's not used in production code? + +* Option 1: Add comments such as **DO NOT USE IN PRODUCTION CODE!**, hoping the exclamation mark will help. +* Option 2: Rely on code reviews. This can be slow, frustrating, and costly. +* Option 3: Make your architecture _executable_ so it can be automatically enforced straight from the editor. You guessed it, that's where Metalama comes in handy. + +The [open-source](https://github.com/postsharp/Metalama.Extensions/tree/HEAD/src/Metalama.Extensions.Architecture) [Metalama.Extensions.Architecture](https://www.nuget.org/packages/Metalama.Extensions.Architecture) package offers several pre-made custom attributes and compile-time APIs that cover many common conventions teams might want to follow. + +## Custom attributes + +Let's assume we have a constructor that slightly modifies the object's behavior to make it more testable. We want to ensure that this constructor is used only in tests. Metalama provides the [CanOnlyBeUsedFrom](https://doc.postsharp.net/etalama/api/metalama-extensions-architecture-aspects-canonlybeusedfromattribute) attribute for this purpose. + +```c# +using Metalama.Extensions.Architecture.Aspects; + +namespace CommonTasks.ValidatingArchitecture +{ + public class OrderShipping + { + private bool isTest; + + public OrderShipping() + { + } + + [CanOnlyBeUsedFrom(Namespaces = new[] {"**.Tests"})] + public OrderShipping(bool isTest) + { + // Used to trigger specific test configuration + this.isTest = isTest; + } + } +} +``` + +If we attempt to create a new `OrderShipping` instance in a namespace that isn't suffixed by `Tests`, we will see a warning. + +![](../metalama-email-course/images/ValidationWarning.jpg) + +## Fabrics + +Suppose we have a project composed of a large number of components. Each of these components is implemented in its own namespace and is made up of several classes. There are so many components that we don't want to have them each in their own project. + +However, we still want to isolate components from each other. Specifically, we want `internal` members of each namespace to be visible only within this namespace. Only `public` members should be accessible outside of its home namespace. + +Additionally, we want `internal` components to be accessible from any test namespace. + +With Metalama, you can validate each namespace by adding a _fabric_ type: a compile-time class that executes within the compiler or the IDE. + +```cs +namespace MyComponent +{ + internal class Fabric : NamespaceFabric + { + public override void AmendNamespace(INamespaceAmender amender) + { + amender.InternalsCanOnlyBeUsedFrom(from => + from.CurrentNamespace().Or(or => or.Type("**.Tests.**"))); + } + } +} +``` + +Now, if some foreign code tries to access an internal API of the `MyComponent` namespace, a warning will be reported. + +In addition to `InternalsCanOnlyBeUsedFrom`, the package also includes `InternalsCannotBeUsedFrom`, `CanOnlyBeUsedFrom`, and `CannotOnlyBeUsedFrom`. You can easily build more rules based on the code model. + +## Conclusion + +We've just seen two examples of how you can validate your code using pre-built Metalama aspects or compile-time APIs. + +Enforcing rules and conventions in this manner allows you to: + +- Eliminate the need for a written set of rules to which everyone must refer. +- Provide immediate feedback to developers within the familiar confines of the IDE itself. +- Improve code reviews as they now only need to focus on the code itself. +- Simplify the codebase because it adheres to consistent rules. + +You can learn more about architecture validation in our online [documentation](https://doc.metalama.net/conceptual/architecture/usage). diff --git a/metalama-awareness-campaign/05_last.md b/metalama-awareness-campaign/05_last.md new file mode 100644 index 0000000..7ee625e --- /dev/null +++ b/metalama-awareness-campaign/05_last.md @@ -0,0 +1,123 @@ +Hi, + +This is the final email in our series introducing you to Metalama. + +Today, let's look into more advanced patterns in C# and explore how you can automate them using Metalama. + +## INotifyPropertyChanged + +If you're building a desktop or mobile app, or even a web app with client-side Blazor, you're likely familiar with the `INotifyPropertyChanged` interface. While it seems simple to implement, it can become cumbersome and error-prone as you add more complex properties and dependencies between objects. + +Enter the `[Observable]` aspect from the open-source [Metalama.Patterns.Observability](https://doc.metalama.net/patterns/observability?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly) package. + +```csharp +[Observable] +public class Person +``` + +This aspect handles `INotifyPropertyChanged` implementation for you, supporting a rich set of scenarios: + +- **Automatic properties:** + + ```csharp + public string? FirstName { get; set; } + ``` + +- **Properties depending on other properties or fields:** + + ```csharp + public string FullName => $"{this.FirstName} {this._lastName}"; + ``` + +- **Properties depending on child objects, such as `Person`:** + + ```csharp + public string FullName => $"{this.Person.FirstName} {this.Person.LastName}"; + ``` + +- **Properties depending on properties of the base type.** + +Consider how much boilerplate code you'd need to properly implement `INotifyPropertyChanged` for these scenarios and how much you would save with Metalama! To see the work Metalama does for you, [check out the diff](https://doc.metalama.net/patterns/observability/standard-cases?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly). + +It's not just about saving code, but also about enhancing code quality. Remember the last bug when you forgot to add a call to `OnPropertyChanged` for a computed property? With `[Observable]`, since dependency discovery is fully automatic, this won't happen any more. + +Read more details about `[Observable]` in our [documentation](https://doc.metalama.net/patterns/observability?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly). + +## Builder Pattern + +Another frequent source of boilerplate code is the [Builder pattern](https://blog.postsharp.net/builder-pattern-with-metalama?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly), which has become ubiquitous in modern C# due to the increased use of immutable types. + +Consider a simple immutable class: + +```csharp +public partial class Song +{ + public string Title { get; } + + public ImmutableArray Artists { get; } +} +``` + +The code supporting the Builder class would look like this: + +```csharp +public partial class Song +{ + private Song(string title, ImmutableArray artists) + { + this.Title = title; + this.Artists = artists; + } + + public Builder ToBuilder() => new Builder(this); + + public class Builder + { + public string Title { get; set; } + + public ImmutableArray.Builder Artists { get; } + + public Builder() + { + this.Artists = ImmutableArray.CreateBuilder(); + } + + public Builder(Song song) + { + this.Title = song.Title; + this.Artists = song.Artists.ToBuilder(); + } + + public Song Build() => new Song(this.Title, this.Artists.ToImmutable()); + } +} +``` + +How repetitive! Do you really want to write this code by hand? Thankfully, [this can be automated using Metalama](https://doc.metalama.net/examples/builder?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly), and you can tailor the code generation pattern to fit your needs. + +## Other Examples + +We can't cover all use cases of Metalama in a single email, so before wrapping up this sequence, here's a list of Metalama use cases: + +- [Parameter validation](https://doc.metalama.net/examples/validation?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly) and [code contracts](https://doc.metalama.net/patterns/contracts); +- [Logging](https://doc.metalama.net/examples/log?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly); +- [Exception handling](https://doc.metalama.net/examples/exception-handling?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly) with or without [Polly](https://doc.metalama.net/examples/exception-handling/retry/retry-5); +- [Caching](https://doc.metalama.net/patterns/caching?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly) and [memoization](https://doc.metalama.net/patterns/memoization?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly); +- [INotifyPropertyChanged](https://doc.metalama.net/patterns/observability), WPF [commands](https://doc.metalama.net/patterns/wpf/command?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly), and [dependency properties](https://doc.metalama.net/patterns/wpf/dependency-property?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly); +- [Architecture verification](https://doc.metalama.net/conceptual/architecture?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly); +- [Change tracking](https://doc.metalama.net/examples/change-tracking?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly) and the [Memento pattern](https://doc.metalama.net/examples/memento?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly). +- [Cloning](https://doc.metalama.net/examples/clone?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly); +- [Builder](https://doc.metalama.net/examples/builde?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly), and [Singleton](https://doc.metalama.net/examples/singleton?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly) patterns; +- [ToString](https://doc.metalama.net/examples/tostring) generation. + +For more use cases and open-source aspect implementations, visit the [Metalama Marketplace](https://www.postsharp.net/metalama/marketplace?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly). + +## How to Get Started? + +Start using Metalama today. Add the `Metalama.Framework` package to your project and activate _Metalama Free_, our free edition that lets you use up to three aspect types (e.g., logging, caching, and `INotifyPropertyChanged`) regardless of your project's size. + +For a better development experience, download the optional [Visual Studio Tools for Metalama](https://www.postsharp.net/metalama/download?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly). + +If you have any questions, feel free to reach out to our community on [GitHub](https://github.com/orgs/postsharp/discussions) or on our [Slack workspace](https://www.postsharp.net/slack?mtm_campaign=awareness&mtm_kwd=email5&mtm_source=instantly). + +Happy meta-programming with Metalama! diff --git a/metalama-email-course/010-required-contract.md b/metalama-email-course/010-required-contract.md new file mode 100644 index 0000000..61131f9 --- /dev/null +++ b/metalama-email-course/010-required-contract.md @@ -0,0 +1,94 @@ +--- +subject: Verifying Required Fields and Parameters With Metalama +--- + +Welcome to the Metalama e-mail course! In this first email, we will explore one of the most straightforward features of Metalama: code contracts. + +Developers frequently need to verify that certain fields, properties, parameters, or return values are not null. Even though the code necessary to perform these checks is not complex, it can lead to clutter in the codebase. + +Consider a typical string property that might look like this: + +```c# +public class ApplicationUser +{ + private string userName; + + public string UserName + { + get { return userName; } + + set + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Invalid value for MyString. Value must not be null or blank."); + } + + userName = value; + } + } +} +``` + +Metalama can streamline this task. Using the same string property as an example, you would only need the following: + +```c# +using Metalama.Patterns.Contracts; + +namespace CommonTasks.Required +{ + public class ApplicationUser + { + [Required] + public string UserName { get; set; } + } +} +``` + +Not only is the code cleaner, but it also becomes immediately apparent that the `UserName` property is required for the application's operation. This inference isn't as quickly made from the first example. + +At compile time, Metalama will introduce all the necessary code to ensure that the `UserName` property is assigned a non-empty value. The following is the code that is _executed_: + +```c# +using Metalama.Patterns.Contracts; + +namespace CommonTasks.Required +{ + public class ApplicationUser + { + private string _userName = default!; + + [Required] + public string UserName + { + get + { + return this._userName; + } + + set + { + if (string.IsNullOrWhiteSpace(value)) + { + if (value == null!) + { + throw new ArgumentNullException("value", "The 'UserName' property is required."); + } + else + { + throw new ArgumentOutOfRangeException("value", "The 'UserName' property is required."); + } + } + + this._userName = value; + } + } + } +} +``` + +As you can see, Metalama generates the boilerplate code that validates the string before it is assigned. + +Metalama provides a broad range of pre-built contracts that you can employ in scenarios where it's necessary to ensure that fields, properties, parameters, or return values meet certain conditions. In every case, all you need to do is add the relevant attribute to your code in the appropriate place, and Metalama will introduce the necessary additional code at compile time. Examples include `[Phone]`, `[Email]`, and `[CreditCard]` for strings, as well as attributes like `[Positive]`, `[StrictlyPositive]` or `[Range]` for numbers. + +Performing these tasks manually can be time-consuming and error-prone. Metalama eliminates the need for writing repetitive code, clarifies your intention to anyone else who reads your code later, and ensures that it will function as expected when required. Because the boilerplate is now generated _on the fly_ at compile time, you no longer need any boilerplate in your _source_ code. Your codebase becomes simpler, easier to read, and easier to maintain. diff --git a/metalama-email-course/015-vsx.md b/metalama-email-course/015-vsx.md new file mode 100644 index 0000000..a74c237 --- /dev/null +++ b/metalama-email-course/015-vsx.md @@ -0,0 +1,62 @@ +--- +subject: "Have You Installed Visual Tools for Metalama?" +--- + +If you are using Visual Studio 2022 (any edition), ensure that you have installed the [Visual Tools for Metalama](https://marketplace.visualstudio.com/items?itemName=PostSharpTechnologies.PostSharp). While not a prerequisite for using Metalama, this tool significantly simplifies the process by offering several useful features in the IDE. + +{: .note } +Visual Tools for Metalama are not open source but are **FREE** for individuals, non-commercial use, and companies with up to 3 users under the Metalama Community license. + +## Metalama Diff + +Primarily, this tool allows you to visualize the impact of Metalama on your code. + +![](images/vsx2.gif) + +The right-click context menu in the editor window provides the _Show Metalama Diff_ option. This command opens a separate editor window, displaying the precise locations and modifications that Metalama will make at compile time. + +For new Metalama users, this feature is incredibly helpful as it reveals exactly how your code will be transformed at compile time. It also ensures that the functionality you want Metalama to add to your code is indeed being incorporated. + +As you start crafting your custom Metalama aspects, this feature becomes even more advantageous, allowing you to see how your aspects are integrated into your codebase. + +## Aspect Explorer + +The Metalama extension also includes the Aspect Explorer tool window. It offers a comprehensive overview of your project and its interaction with Metalama. You can access the viewer through the extensions menu. + +![](images/aspectViewer.png) + +The Aspect Explorer comprises three panes. + +![](images/aspectViewer1.png) + +The top pane displays all the aspects that are available to the project. It allows you to see all potential aspects and serves as a straightforward way to explore the available aspects within Metalama libraries (such as the `Metalama.Patterns.Contracts` library) without needing to consult the documentation. + +In the central pane, you can identify which parts of your project's code are influenced by aspects. To use this pane, you must first select the aspect of interest in the upper pane. + +{: .note } +If you apply aspects to the return value of methods, they will not appear in the Affected Code pane. + +## CodeLens + +Another useful feature of this extension is its seamless integration with Visual Studio's code lens feature. + +In the brief clip below, you'll see a class implementing an interface where Metalama aspects have been applied to some properties. While it's not immediately clear that the aspects have been inherited, a closer look reveals that the code lens feature confirms this. The 'Show Metalama Diff' command further corroborates it. + +![](images/vsx3.gif) + +![](images/us1.jpg) + +This tool also offers syntax highlighting for specific Metalama keywords, which is especially beneficial when creating your custom aspects. + +{: .note } +Currently, there are no similar equivalents of this tool for either VSCode or JetBrains' Rider IDE. + +## Aspect Syntax Highlighting + +The Metalama Tools for Visual Studio 2022 extension is available at no cost. New Metalama users will find this tool insightful, as it demonstrates what Metalama does precisely. It shows the amount of standard boilerplate code it writes on your behalf, saving you time and preserving the clarity of your codebase. + +Experienced Metalama users will appreciate both the syntax highlighting and the ability to see how their custom aspects are likely to interact with other third-party code. + +## Summary + +If you are using Visual Studio 2022, don't miss the [Visual Tools for Metalama](https://marketplace.visualstudio.com/items?itemName=PostSharpTechnologies.PostSharp). It offers plenty of features to make your work with Metalama easier. diff --git a/metalama-email-course/030-caching.md b/metalama-email-course/030-caching.md new file mode 100644 index 0000000..706036a --- /dev/null +++ b/metalama-email-course/030-caching.md @@ -0,0 +1,172 @@ +--- +subject: The Simplest Way to Cache a Method's Return Value +--- + +Caching is a technique used to optimize application performance by storing data that changes infrequently or is expensive to retrieve in an intermediate layer. + +Implementing caching can be challenging, as it requires several components to work together seamlessly. Metalama simplifies this process significantly. + +## Adding Caching to Your App + +Metalama supports caching with and without Dependency Injection (DI). In our first example, we will explore its usage in a project that employs DI. + +You can add caching to your app in just three steps: + +1. Add the [Metalama.Patterns.Caching.Aspects](https://www.nuget.org/packages/Metalama.Patterns.Caching.Aspects/) package to your project. +2. Navigate to all methods that need caching and add the `[Cache]` custom attribute. +3. Go to the application startup code and call `AddCaching`, which adds the `ICachingService` interface to your `IServiceCollection`, enabling the `[Cache]` aspect to be used on all objects instantiated by the DI container. This code is fairly standard and omitted here for brevity, but you can find it in the [documentation](https://doc.metalama.net/patterns/caching/getting-started). + +Let's examine what the `[Cache]` attribute does to your code. Consider the following example: + +```c# +public sealed class CloudCalculator +{ + [Cache] + public int Add(int a, int b) + { + Console.WriteLine("Doing some very hard work."); + + this.OperationCount++; + + Console.WriteLine("Finished doing some very hard work."); + + return a + b; + } + + public int OperationCount { get; private set; } +} +``` + +At build time, Metalama transforms it into the following: + +```c# +public sealed class CloudCalculator +{ + [Cache] + public int Add(int a, int b) + { + static object? Invoke(object? instance, object?[] args) + { + return ((CloudCalculator)instance).Add_Source((int)args[0], (int)args[1]); + } + + return this._cachingService!.GetFromCacheOrExecute( + CloudCalculator._cacheRegistration_Add!, this, new object[] { a, b }, Invoke); + } + + private int Add_Source(int a, int b) + { + Console.WriteLine("Doing some very hard work."); + + this.OperationCount++; + + Console.WriteLine("Finished doing some very hard work."); + + return a + b; + } + + public int OperationCount { get; private set; } + + private static readonly CachedMethodMetadata _cacheRegistration_Add; + + private ICachingService _cachingService; + + static CloudCalculator() + { + // Skipped for brevity. + } + + public CloudCalculator(ICachingService? cachingService = default) + { + this._cachingService = cachingService + ?? throw new System.ArgumentNullException(nameof(cachingService)); + } +} +``` + +As you can see, Metalama moves the original method implementation of `Add` into `Add_Source` (we would typically name it `AddCore` if the code were hand-written) and replaces `Add` with a call to the caching service. + +Our potentially expensive operation is invoked using the following code: + +```c# +public void Execute() +{ + for (var i = 0; i < 5; i++) + { + var value = this._cloudCalculator.Add(1, 1); + Console.WriteLine($"CloudCalculator returned {value}."); + } + + Console.WriteLine( + $"In total, CloudCalculator performed {this._cloudCalculator.OperationCount} operation(s)."); +} +``` + +When executed, it produces the following output: + +```text +Doing some very hard work. +Finished doing some very hard work. +CloudCalculator returned 2. +CloudCalculator returned 2. +CloudCalculator returned 2. +CloudCalculator returned 2. +CloudCalculator returned 2. +In total, CloudCalculator performed 1 operation(s). +``` + +From the transformed code, it is evident that Metalama has automatically incorporated the `ICachingService` interface into the `CloudCalculator`. The output shows that the `CloudCalculator` was executed once, its result was cached, and the cached result was used on subsequent calls. + +Without a doubt, Metalama significantly simplifies the process of implementing caching. + +## Without Dependency Injection + +Not every project requires or warrants the use of Dependency Injection, and it cannot be used for caching static methods. However, Metalama Caching can be used without DI. + +This can be achieved by adding a small configuration file to your project: + +```c# +using Metalama.Patterns.Caching.Aspects; + +// Disable dependency injection. +[assembly: CachingConfiguration(UseDependencyInjection = false)] +``` + +The Metalama [documentation](https://doc.metalama.net/patterns/caching/getting-started) illustrates the same example as above, but without using Dependency Injection. As before, caching can be added via a single `[Cache]` attribute, and it will produce the same result. + +## Configuring Caching + +Metalama not only simplifies the implementation of caching but also provides ways to customize your cache keys and exclude certain parameters. + +Here are a few topics to explore: + +- [Customizing caching keys](https://doc.metalama.net/patterns/caching/caching-keys) +- [Excluding parameters](https://doc.metalama.net/patterns/caching/exclude-parameters) +- [Configuring expiration, priority, or auto-reload](https://doc.metalama.net/patterns/caching/configuring) + +## Invalidating the Cache + +As Phil Karlton once [famously said](https://www.karlton.org/2017/12/naming-things-hard/), "There are only two hard things in Computer Science: cache invalidation and naming things." + +Metalama Caching offers two approaches to cache invalidation: + +- **Explicit** cache invalidation by using the `[InvalidateCache]` custom attribute or the `ICachingService.Invalidate` method. Both approaches are type-safe, but they have drawbacks: the _Update_ methods must know which _Read_ methods they must invalidate, and this must be kept in sync. +- **Implicit** cache invalidation through **cache dependencies**, where Metalama builds a dependency graph. This approach provides better separation of concerns between the _Update_ and _Read_ layers but comes at the cost of higher performance and memory overhead due to the need to maintain this graph. + +For details, see [Invalidating the cache](https://doc.metalama.net/patterns/caching/invalidation) in the documentation. + +## Distributed Caching + +If you have a distributed application that could benefit from caching, Metalama has you covered with three possible topologies: + +- Server or cloud caching with [Redis](https://doc.metalama.net/patterns/caching/redis). +- Hybrid caching, where an in-memory cache stands in front of a Redis cache. +- Several local in-memory caches (typically on different nodes of your app) synchronized over [Azure Service Bus](https://doc.metalama.net/patterns/caching/pubsub). + +Azure and Redis adapters for Metalama Caching require a Metalama Professional license. + +## Conclusion + +Applying caching to an application can dramatically improve performance, but implementing the pattern is not straightforward. Metalama does all the heavy lifting for you and provides several flexible implementations that you can customize to meet your specific requirements. + +By leveraging Metalama Caching, you'll find that implementing caching is both simpler and more efficient than creating a bespoke solution. Metalama Caching is completely configurable and extensible and comes with proprietary but source-available extensions for Azure and Redis. diff --git a/metalama-email-course/051-memoization.md b/metalama-email-course/051-memoization.md new file mode 100644 index 0000000..7372aec --- /dev/null +++ b/metalama-email-course/051-memoization.md @@ -0,0 +1,95 @@ +--- +subject: "Memoization: A Simplified and Faster Form of Caching" +--- + +In a previous example, we explored the use of `Metalama.Patterns.Caching.Aspects` to cache the return values of methods based on their arguments. This caching approach relies on generating a unique `string` as the cache key. While effective for accelerating slow methods, it may not be as efficient for speeding up already fast methods. + +Fortunately, Metalama provides an alternative: _memoization_. Memoization is available for read-only properties or parameterless methods. It does not rely on generating a cache key, nor does it use an in-memory synchronized dictionary or out-of-process storage. Instead, it stores the cached value directly within the current object, offering almost instant access with no per-access memory allocation. + +## Caching or Memoization: How to Choose? + +The table below highlights the key differences between memoization and caching: + +| Factor | Memoization | Caching | +| ---------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | +| Scope | Local to a single class instance within the current process. | Can be local or shared, such as when using an external service like Redis. | +| Method Parameters | Not supported. | Supported. | +| Complexity and Overhead | Minimal overhead. | Significant overhead due to cache key generation and, in the case of distributed caching, serialization. | +| Expiration and Invalidation | Not supported. | Offers advanced and configurable expiration policies and invalidation APIs. | + +From this comparison, it is clear that memoization is the better choice for simple scenarios, providing a lightweight form of caching. + +## Example + +To use memoization with Metalama, add the `Metalama.Patterns.Memoization` library to your project and apply the `[Memoize]` attribute where needed. Consider the following example, where the goal is to ensure that the `Hash` property and `ToString()` method (which are called very frequently) perform their computations and allocate memory only once. + +```c# +public class HashedBuffer +{ + public HashedBuffer(ReadOnlyMemory buffer) + { + this.Buffer = buffer; + } + + public ReadOnlyMemory Buffer { get; } + + [Memoize] + public ReadOnlyMemory Hash => XxHash64.Hash(this.Buffer.Span); + + [Memoize] + public override string ToString() => $"HashedBuffer ({this.Buffer.Length} bytes)"; +} +``` + +At compile time, this code is transformed into: + +```c# +public class HashedBuffer +{ + public HashedBuffer(ReadOnlyMemory buffer) + { + this.Buffer = buffer; + } + + public ReadOnlyMemory Buffer { get; } + + [Memoize] + public ReadOnlyMemory Hash + { + get + { + if (this._Hash == null) + { + var value = new StrongBox>(this.Hash_Source); + Interlocked.CompareExchange(ref this._Hash, value, null); + } + + return _Hash!.Value; + } + } + + private ReadOnlyMemory Hash_Source => XxHash64.Hash(this.Buffer.Span); + + [Memoize] + public override string ToString() + { + if (this._ToString == null) + { + string value; + value = $"HashedBuffer ({this.Buffer.Length} bytes)"; + Interlocked.CompareExchange(ref this._ToString, value, null); + } + + return _ToString; + } + + private StrongBox> _Hash; + private string _ToString; +} +``` + +As shown, this is a much simpler caching implementation, which may be sufficient for relatively straightforward scenarios. + +## Summary + +Memoization is one of the simplest ways to accelerate CPU-intensive applications without adding boilerplate code. It is even simpler and uses less memory than the `Lazy` class. diff --git a/metalama-email-course/052-naming-conventions.md b/metalama-email-course/052-naming-conventions.md new file mode 100644 index 0000000..243ce29 --- /dev/null +++ b/metalama-email-course/052-naming-conventions.md @@ -0,0 +1,118 @@ +--- +subject: "Validating Naming Conventions" +--- + + +In previous emails, we demonstrated how Metalama can generate boilerplate code on-the-fly during compilation, automating the implementation of repetitive but necessary code. However, code generation is just one of Metalama's capabilities. In this email, we will explore Metalama's second pillar: its ability to validate source code against architectural rules, starting with naming conventions. + +{: .note } +Architecture validation is available only with Metalama Professional and is not included in the open-source version. Our next email will return to open-source features and use cases. + +## Why Care About Naming Conventions? + +Adhering to naming conventions keeps code clean and understandable, whether you're working in a team or independently. It's like maintaining a tidy room: it helps everyone, including your future self, quickly find what they need without confusion. + +While your IDE can enforce basic naming conventions like casing or prefixes, and `.editorconfig` can configure code style, there is no standard tool to verify the meaning of names themselves. For example, aside from special cases like collections or dictionaries, naming conventions often require custom validation. + +Well-named types and methods can communicate their purpose and functionality clearly. A common rule is that types should have a suffix indicating their role. + +## Enforcing Naming Conventions Using a Custom Attribute + +Metalama makes it easy to enforce naming conventions in your codebase—in real time, directly within the IDE. + +To enforce naming conventions, we will use the `Metalama.Extensions.Architecture` package. Make sure to add it to your project first. + +For this example, suppose we have a base class `Entity` that all entity classes must derive from. The team decides that all entity classes must have the `Entity` suffix in their names. + +To enforce this convention, simply add the `[DerivedTypesMustRespectNamingConvention]` attribute to the `Entity` class: + +```c# +using Metalama.Extensions.Architecture.Aspects; + +[DerivedTypesMustRespectNamingConvention("*Entity")] +public abstract class Entity +{ + public string Id { get; } + public abstract string Description { get; } + + protected Entity(string id) + { + Id = id; + } +} +``` + +From this point on, a warning will be issued for any class derived from `Entity` that does not follow the naming convention. + +For example, consider the following code: + +```c# +public class Customer : Entity +{ + public Customer(string id, string name) : base(id) + { + this.Name = name; + } + + public string Name { get; } + + public override string Description => this.Name; +} +``` + +Since the `Customer` class does not follow the naming convention, a warning is immediately displayed. + +![](images/attribute-namingconvention.png) + +## Enforcing Naming Conventions with a Fabric + +What if you don't own the source code of the base class or interface for which you want to enforce a naming convention? What if the type comes from a library? + +For instance, imagine an application that heavily uses stream readers, with several classes created by different team members to implement these readers for various tasks. The team decides that all such classes must have the `StreamReader` suffix for clarity. + +Fabrics are an excellent tool for enforcing naming conventions on types you don't own. Fabrics are compile-time classes executed within the compiler or IDE. Fabrics derived from `ProjectFabric` apply to an entire project. + +Here's how to create a fabric to enforce this naming convention: + +```c# +using Metalama.Extensions.Architecture.Fabrics; +using Metalama.Framework.Fabrics; + +internal class NamingConvention : ProjectFabric +{ + public override void AmendProject(IProjectAmender amender) + { + amender + .SelectTypesDerivedFrom(typeof(StreamReader)) + .MustRespectNamingConvention("*Reader"); + } +} +``` + +In the code above, the fabric examines each class in the project derived from `StreamReader`. If any such class does not have a name ending in `Reader`, a warning is displayed. + +With this custom validation rule in place, let's test it. In the code below, we have two classes derived from `StreamReader`. One follows the naming convention, while the other does not, triggering a warning. + +```c# +internal class FancyStream : StreamReader +{ + public FancyStream(Stream stream) : base(stream) + { + } +} + +internal class SuperFancyStreamReader : StreamReader +{ + public SuperFancyStreamReader(Stream stream) : base(stream) + { + } +} +``` + +The warning is displayed as shown below: + +![](images/naming-conventions-1.gif) + +## Summary + +Although these examples are simple, they demonstrate how Metalama can help validate your codebase and enforce architectural rules. For more information, refer to the [Metalama Documentation](https://doc.metalama.net/conceptual/architecture/naming-conventions). diff --git a/metalama-email-course/055-TODO-more-contracts.md b/metalama-email-course/055-TODO-more-contracts.md new file mode 100644 index 0000000..7347303 --- /dev/null +++ b/metalama-email-course/055-TODO-more-contracts.md @@ -0,0 +1,6 @@ +Revised Selection of Two Non-Selected Articles: + +* Avoiding Insistence on Inheritance +* Creating a `User` class with a `Password` property. +* Establishing a `UserFactory` class with a `Create` method that accepts a password argument. +* Returning Value: `IPasswordGenerator` interface, `PasswordGenerator` implementation. diff --git a/metalama-email-course/060-creating-aspects-intro.md b/metalama-email-course/060-creating-aspects-intro.md new file mode 100644 index 0000000..c0da139 --- /dev/null +++ b/metalama-email-course/060-creating-aspects-intro.md @@ -0,0 +1,138 @@ +--- +subject: Introduction to Creating Custom Aspects +--- + + +In the previous segments of this e-mail course, we explored several pre-built aspects available through downloadable libraries. However, the diverse scenarios developers face may not always be addressed by these pre-built aspects. Therefore, it's time to learn how to create your own aspects. Since most pre-built aspects are open source, this knowledge will also enable you to customize them to suit your specific needs. + +## What Can an Aspect Do? + +Aspects can be thought of as units of compile-time behavior capable of performing three main tasks: +1. Generating code +2. Reporting errors and warnings +3. Suggesting code fixes or refactorings, typically as a remediation for an error or warning. + +_Code generation_ is undoubtedly the most complex area. Below are the different types of transformations you can apply to code using Metalama: + +- Overriding existing members +- Introducing new members into an existing type +- Implementing interfaces in an existing type +- Adding custom attributes +- Adding class or instance initializers +- Adding parameters to an existing constructor and propagating them through upstream constructors +- Adding validation or normalization logic to parameters or return values +- Introducing new types (nested or top-level) + +Let’s start with the most commonly used type of code generation: overriding existing members. + +## Abstract Types for Member Overrides + +Metalama provides several abstract classes depending on the type of member you want to override. + +For __methods__, create a class that derives from the `OverrideMethodAspect` class. + +```c# +using Metalama.Framework.Aspects; + +internal class MyAspectAttribute : OverrideMethodAspect +{ + public override dynamic? OverrideMethod() + { + throw new NotImplementedException(); + } +} +``` + +For __fields or properties__, use the `OverrideFieldOrPropertyAspect` class. + +```c# +using Metalama.Framework.Aspects; + +internal class MyAspectAttribute : OverrideFieldOrPropertyAspect +{ + public override dynamic? OverrideProperty + { + get; + set; + } +} +``` + +Lastly, for __events__, use the `OverrideEventAspect` class. + +```c# +using Metalama.Framework.Aspects; + +internal class MyAspectAttribute : OverrideEventAspect +{ + public override void OverrideAdd(dynamic value) + { + throw new NotImplementedException(); + } + + public override void OverrideRemove(dynamic value) + { + throw new NotImplementedException(); + } +} +``` + +Each of these classes defines a set of abstract _templates_ that you must implement: `OverrideMethod`, `OverrideProperty`, and `OverrideAdd`. The code in these templates will _replace_ the code of the members to which you apply the templates. Within these templates, you can use `meta.Proceed()` to invoke the _original_ code of the method. To access information about the overridden member, your template can use the API behind `meta.Target`. Here, `meta` stands for meta-programming. + +## Example: Authorization + +To illustrate, let’s consider a simplified authorization aspect. This aspect adds a check to restrict access to specific methods to the user 'Mr Bojangles'. If the current user is not 'Mr Bojangles', an exception is thrown. Otherwise, the method proceeds with its normal execution using `return meta.Proceed();`. + +```c# +using Metalama.Framework.Aspects; +using System.Security; +using System.Security.Principal; + +internal class MyAspectAttribute : OverrideMethodAspect +{ + public override dynamic? OverrideMethod() + { + // Determine the current user + var user = WindowsIdentity.GetCurrent().Name; + + if (user != "Mr Bojangles") + { + throw new SecurityException($"The '{meta.Target.Method}' method can only be called by Mr Bojangles."); + } + + // Proceed with the method execution + return meta.Proceed(); + } +} +``` + +In practice, the custom aspect is applied as an attribute on a method: + +```c# +[MyAspect] +private static void HelloFromMrBojangles() +{ + Console.WriteLine("Hello"); +} +``` + +Using the 'Show Metalama Diff' tool, you can examine the code that will be added at compile time. This is an excellent way to verify that your custom aspects meet your requirements: + +```c# +[MyAspect] +private static void HelloFromMrBojangles() +{ + var user = WindowsIdentity.GetCurrent().Name; + + if (user != "Mr Bojangles") + { + throw new SecurityException("The 'HelloFromMrBojangles()' method can only be called by Mr Bojangles."); + } + + Console.WriteLine("Hello"); +} +``` + +While this is a simple example, it demonstrates that creating custom aspects is not as daunting as it may seem. + +The Metalama Documentation provides comprehensive information on [creating custom aspects](https://doc.metalama.net/conceptual/aspects). diff --git a/metalama-email-course/070-custom-logging-aspect.md b/metalama-email-course/070-custom-logging-aspect.md new file mode 100644 index 0000000..2d14404 --- /dev/null +++ b/metalama-email-course/070-custom-logging-aspect.md @@ -0,0 +1,206 @@ +--- +subject: Logging Methods, Including Parameter Values +--- + +In a previous article, we demonstrated a simple example of `OverrideMethodAspect` that performed authorization. This example made minimal use of the `meta` model: it only invoked `meta.Proceed()` to continue with the method execution and used `meta.Target.Method.ToString()` to print the method name. + +Today, we will dive deeper into logging and show how your meta-code can be much more expressive. + +In this example, we will log not only the name of the method being called but also any parameters (along with their types) that are passed into it, as well as any return value, if applicable. + +For now, we will output the messages to the console as _interpolated strings_. To facilitate this, we will create a helper method that generates an interpolated string containing the overridden method (exposed as `meta.Target.Method`) and its parameters (exposed as `meta.Target.Method.Parameters`). + +```c# +using Metalama.Framework.Code; +using Metalama.Framework.Code.SyntaxBuilders; + +public partial class LogAttribute +{ + private static InterpolatedStringBuilder BuildInterpolatedString(bool includeOutParameters) + { + var stringBuilder = new InterpolatedStringBuilder(); + + // Include the type and method name. + stringBuilder.AddText(meta.Target.Type.ToDisplayString(CodeDisplayFormat.MinimallyQualified)); + stringBuilder.AddText("."); + stringBuilder.AddText(meta.Target.Method.Name); + stringBuilder.AddText("("); + var i = 0; + + // Include a placeholder for each parameter. + foreach (var p in meta.Target.Method.Parameters) + { + var comma = i > 0 ? ", " : ""; + + if (p.RefKind == RefKind.Out && !includeOutParameters) + { + // When the parameter is 'out', we cannot read its value. + stringBuilder.AddText($"{comma}{p.Name} = "); + } + else + { + // Otherwise, add the parameter value. + stringBuilder.AddText($"{comma}{p.Name} = "); + stringBuilder.AddExpression(p.Value); + stringBuilder.AddText("}"); + } + + i++; + } + + stringBuilder.AddText(")"); + + return stringBuilder; + } +} +``` + +Now that we have our `InterpolatedStringBuilder`, we can create a revised logging aspect that utilizes it. + +```c# +using Metalama.Framework.Aspects; +using Metalama.Framework.Code; +using Metalama.Framework.Code.SyntaxBuilders; + +public partial class LogAttribute : OverrideMethodAspect +{ + public override dynamic? OverrideMethod() + { + // Write the entry message. + var entryMessage = BuildInterpolatedString(false); + entryMessage.AddText(" started."); + Console.WriteLine(entryMessage.ToValue()); + + try + { + // Invoke the method and store the result in a variable. + var result = meta.Proceed(); + + // Display the success message. The message differs when the method is void. + var successMessage = BuildInterpolatedString(true); + + if (meta.Target.Method.ReturnType.Is(typeof(void))) + { + // When the method is void, display a constant text. + successMessage.AddText(" succeeded."); + } + else + { + // When the method has a return value, add it to the message. + successMessage.AddText(" returned "); + successMessage.AddExpression(result); + successMessage.AddText("."); + } + + Console.WriteLine(successMessage.ToValue()); + + return result; + } + catch (Exception e) + { + // Display the failure message. + var failureMessage = BuildInterpolatedString(false); + failureMessage.AddText(" failed: "); + failureMessage.AddExpression(e.Message); + Console.WriteLine(failureMessage.ToValue()); + + throw; + } + } +} +``` + +In this aspect, we log the name of the method and any parameters passed to it. The method then executes, and we log the return value if the method is not void or an error message if an exception occurs. + +As you can see, Metalama allows you to write complex templates with the full power of C# available at compile time for authoring templates. We call this C#-to-C# template language _T#_. + +When the `[Log]` attribute is applied to the following code: + +```c# +public static class Calculator +{ + [Log] + private static double Divide(int a, int b) + { + return a / b; + } + + [Log] + public static void IntegerDivide(int a, int b, out int quotient, out int remainder) + { + quotient = a / b; + remainder = a % b; + } +} +``` + +That code will then be transformed at compile time to this: + +```c# +[Log] +public static double Divide(int a, int b) +{ + Console.WriteLine($"Calculator.Divide(a = {a}, b = {b}) started."); + + try + { + double result; + result = a / b; + + Console.WriteLine($"Calculator.Divide(a = {a}, b = {b}) returned {result}."); + return (double)result; + } + catch (Exception e) + { + Console.WriteLine($"Calculator.Divide(a = {a}, b = {b}) failed: {e.Message}"); + throw; + } +} + +[Log] +public static void IntegerDivide(int a, int b, out int quotient, out int remainder) +{ + Console.WriteLine($"Calculator.IntegerDivide(a = {a}, b = {b}, quotient = , remainder = ) started."); + + try + { + quotient = a / b; + remainder = a % b; + + Console.WriteLine($"Calculator.IntegerDivide(a = {a}, b = {b}, quotient = {quotient}, remainder = {remainder}) succeeded."); + return; + } + catch (Exception e) + { + Console.WriteLine($"Calculator.IntegerDivide(a = {a}, b = {b}, quotient = , remainder = ) failed: {e.Message}"); + throw; + } +} +``` + +Now, if we execute the following code: + +```csharp +try +{ + Calculator.Divide(7, 3); + Calculator.IntegerDivide(7, 3, out int quotient, out int remainder); +} +catch (Exception ex) +{ + Console.WriteLine(ex); +} +``` + +The console will output the following: + +```text +Program output + +Calculator.Divide(a = 7, b = 3) started. +Calculator.Divide(a = 7, b = 3) returned 2.3333333333333335. +Calculator.IntegerDivide(a = 7, b = 3, quotient = , remainder = ) started. +Calculator.IntegerDivide(a = 7, b = 3, quotient = 2, remainder = 1) succeeded. +``` + +You now know how to create non-trivial templates with T#, Metalama's C#-to-C# template language. diff --git a/metalama-email-course/090-add-logging-with-fabric.md b/metalama-email-course/090-add-logging-with-fabric.md new file mode 100644 index 0000000..da7d4e2 --- /dev/null +++ b/metalama-email-course/090-add-logging-with-fabric.md @@ -0,0 +1,64 @@ +--- +suject: "Using Metalama: Project Fabrics" +--- + + +In previous discussions, we used a custom attribute to add aspects to a target class or method. We identified individual methods requiring logging and added the `[Log]` custom attribute. + +In a real-world application, you may encounter dozens of classes and hundreds of methods. To achieve comprehensive logging across the application, you would need to go through each class individually, adding the `[Log]` attribute to every method. While this process is far more efficient than manually writing logging code for each method, it is still a significant task to manually traverse each class and add the attribute. + +Fortunately, Metalama provides a mechanism to automate this process, known as _fabrics_. + +In the project, add a new class. The name of the class is not important, but it must inherit from `ProjectFabric`. + +```c# +using Metalama.Framework.Fabrics; + +namespace UsingMetalama.Fabrics +{ + internal class LogDistribution : ProjectFabric + { + public override void AmendProject(IProjectAmender amender) + { + throw new NotImplementedException(); + } + } +} +``` + +From this basic implementation, it is clear that this class will modify the current project. Now, let's enhance it to perform a meaningful function. + +```c# +using Metalama.Framework.Fabrics; + +namespace UsingMetalama.Fabrics +{ + internal class LogDistribution : ProjectFabric + { + public override void AmendProject(IProjectAmender amender) + { + amender + .SelectMany(t => t.AllTypes) + .SelectMany(t => t.Methods) + .AddAspectIfEligible(); + } + } +} +``` + +In simple terms, the code above selects every class in the project, then selects each method in each class. If applicable, it adds the `Log` attribute to that method. + +Using the Metalama Tools Extension for Visual Studio, you can observe how this simple `ProjectFabric` effectively modifies your code. + +![](images/fabric2.jpg) + +Although this is a basic example, it demonstrates the power of Metalama and its potential as a time-saving tool. + +We have only scratched the surface of what is possible with fabrics. For example, you could apply the `Log` attribute to all methods across an entire solution of projects using a `TransitiveFabric`. + +If you wanted to target a specific type or namespace, you could achieve this with either a `TypeFabric` or a `NamespaceFabric`. + +Fabrics are not only useful for adding aspects to your code but can also be used to enforce architectural rules in your codebase. + +You can read more about fabrics [here](https://doc.metalama.net/conceptual/using/fabrics). While fabrics are one of Metalama's more advanced features, understanding how they work will enable you to accomplish tasks that might have previously seemed nearly impossible. + diff --git a/metalama-email-course/100-retry-aspect-parameters.md b/metalama-email-course/100-retry-aspect-parameters.md new file mode 100644 index 0000000..717666f --- /dev/null +++ b/metalama-email-course/100-retry-aspect-parameters.md @@ -0,0 +1,182 @@ +--- +subject: "Auto-Retry Aspect with Aspect Parameters" +--- + +Today, we will explore how to make your aspect parametric and apply this technique to a new aspect type: auto-retry. + +It's not uncommon for a method to fail, not due to inherent issues with the method's code, but because of unpredictable external circumstances. A common example is when connecting to an external data source or API. Instead of allowing the method to fail and immediately throw an exception that requires handling, it might be more appropriate to retry the operation. + +With this in mind, let's outline some basic functionalities this aspect should have: + +- If an error occurs outside the direct control of the method, the aspect should attempt to retry the operation. +- It should be possible to specify the number of retry attempts. +- Ideally, there should be a delay between each attempt to allow the external fault to correct itself (e.g., an intermittent internet connection). This delay should be configurable. + +We want our _retry_ aspect to accept two parameters: the maximum number of attempts and the delay between attempts. + +{. note } +Because aspects add code at compile time, you can only set input parameters ahead of compilation. End users of an application will not be able to modify these. + +The aspect will only apply to methods, so we already know what its main signature will look like: + +```c# +public class RetryAttribute : OverrideMethodAspect +{ + public override dynamic? OverrideMethod() + { + throw new NotImplementedException(); + } +} +``` + +Since it needs to accept parameters, it will require a constructor to take them and a place to store them: + +```c# +public class RetryAttribute : OverrideMethodAspect +{ + public RetryAttribute(int attempts, int millisecondsOfDelay) + { + this.Attempts = attempts; + this.MillisecondsOfDelay = millisecondsOfDelay; + } + + public override dynamic? OverrideMethod() { throw new NotImplementedException(); } + + public int Attempts { get; set; } + public int MillisecondsOfDelay { get; set; } +} +``` + +Now we can flesh out the functionality of the aspect: + +```c# +/// +/// Retries the task at hand by the number of times specified in the attempts parameter. +/// For each attempt, the delay in milliseconds is doubled. +/// +public class RetryAttribute : OverrideMethodAspect +{ + /// + /// Constructor. + /// + /// + /// The maximum number of times the method should be executed. + /// + /// + /// The delay, in milliseconds, to wait between the first and second attempts. + /// The delay is doubled for each subsequent attempt. + /// + public RetryAttribute(int attempts = 3, int millisecondsOfDelay = 1000) + { + this.Attempts = attempts; + this.MillisecondsOfDelay = millisecondsOfDelay; + } + + public override dynamic? OverrideMethod() + { + for (var i = 0; ; i++) + { + try + { + return meta.Proceed(); + } + catch (Exception e) when (i < this.Attempts) + { + var delay = this.MillisecondsOfDelay * Math.Pow(2, i + 1); + Console.WriteLine($"{e.Message} Waiting {delay} ms."); + Thread.Sleep((int)delay); + } + } + } + + /// + /// The maximum number of times the method should be executed. + /// + public int Attempts { get; set; } + + /// + /// The delay, in milliseconds, to wait between the first and second attempts. + /// The delay is doubled for each subsequent attempt. + /// + public int MillisecondsOfDelay { get; set; } +} +``` + +In the above example, we add a delay (doubled with each failure) each time the task fails. For tasks like connecting to an API with intermittent internet issues, the additional delay provides time for the connection to be restored and the method to succeed. + +For example, the following method: + +```c# +private static int attempts; + +[Retry(5, 100)] +static bool ConnectToApi(string key) +{ + attempts++; + + Console.WriteLine($"Connecting... attempt #{attempts}."); + + if (attempts <= 4) + { + throw new InvalidOperationException(); + } + + Console.WriteLine("Success, Connected"); + + return true; +} +``` + +Is converted at compile time to: + +```c# +private static int attempts; + +[Retry(5, 100)] +static bool ConnectToApi(string key) +{ + for (var i = 0; ; i++) + { + try + { + attempts++; + + Console.WriteLine($"Connecting... attempt #{attempts}."); + + if (attempts <= 4) + { + throw new InvalidOperationException(); + } + + Console.WriteLine("Success, Connected"); + + return true; + } + catch (Exception e) when (i < 5) + { + var delay = 100 * Math.Pow(2, i + 1); + Console.WriteLine($"{e.Message} Waiting {delay} ms."); + Thread.Sleep((int)delay); + } + } +} +``` + +Notice how the aspect hard-codes the attribute's parameter inputs into the final compiled code. + +This is a relatively simple example, but it illustrates how custom aspects can perform complex tasks. Here's the result when run: + +``` +Connecting... attempt #1. +Operation is not valid due to the current state of the object. Waiting 200 ms. +Connecting... attempt #2. +Operation is not valid due to the current state of the object. Waiting 400 ms. +Connecting... attempt #3. +Operation is not valid due to the current state of the object. Waiting 800 ms. +Connecting... attempt #4. +Operation is not valid due to the current state of the object. Waiting 1600 ms. +Connecting... attempt #5. +Success, Connected +``` + +The Metalama Documentation provides a wealth of information on [creating custom aspects](https://doc.metalama.net/conceptual/aspects). diff --git a/metalama-email-course/105-custom-logging-aspect-with-di.md b/metalama-email-course/105-custom-logging-aspect-with-di.md new file mode 100644 index 0000000..25590b2 --- /dev/null +++ b/metalama-email-course/105-custom-logging-aspect-with-di.md @@ -0,0 +1,149 @@ +--- +subject: "Practical Logging with Dependency Injection" +--- + +Logging is often used as a _Hello, world_ example for aspect-oriented programming, and this email course is no exception. In previous emails, we took the simple approach of using `Console.WriteLine`. However, in production code, you would typically use a more robust solution. Nowadays, the most common way to log is by using the `ILogger` interface from the `Microsoft.Extensions.Logging` namespace. + +To use `ILogger`, you need to introduce a dependency on `ILogger` in your aspect. This is straightforward with Metalama. Simply add a field of type `ILogger` to your aspect and annotate it with the `[IntroduceDependency]` attribute. + +Let’s see this in action with a simplified logging aspect: + +```c# +using Metalama.Extensions.DependencyInjection; +using Metalama.Framework.Aspects; +using Metalama.Framework.Code; +using Metalama.Framework.Code.SyntaxBuilders; +using Microsoft.Extensions.Logging; + +public class LogAttribute : OverrideMethodAspect +{ + [IntroduceDependency] + private readonly ILogger? _logger; + + public override dynamic? OverrideMethod() + { + // Determine if tracing is enabled. + var isTracingEnabled = this._logger?.IsEnabled(LogLevel.Trace) == true; + + // Write entry message. + if (isTracingEnabled) + { + this._logger.LogTrace($"{meta.Target.Method}: Executing."); + } + + try + { + // Invoke the method and store the result in a variable. + var result = meta.Proceed(); + + if (isTracingEnabled) + { + this._logger.LogTrace($"{meta.Target.Method}: Completed."); + } + + return result; + } + catch (Exception e) when (this._logger?.IsEnabled(LogLevel.Warning) == true) + { + // Log the failure message. + this._logger.LogWarning($"{meta.Target.Method}: {e.Message}."); + throw; + } + } +} +``` + +We intentionally made the `_logger` field nullable, which implicitly makes the `ILogger` dependency optional. This ensures that our code can function even without logging. + +The `OverrideMethod` method is a Metalama template that allows us to blend runtime and compile-time code. This enables us to add the `isTracingEnabled` boolean variable (set by calling `ILogger.IsEnabled`). At runtime, this variable determines whether any logging will actually occur. Since logging can be an expensive process, it’s better to log sparingly while retaining the option to log comprehensively when needed. + +In our aspect, most of the logging is conditionally wrapped around the _Trace_ log level, which is rarely enabled. However, exception logging is wrapped around the _Warning_ log level, which is almost always enabled. This ensures that errors are consistently recorded in the log. + +When applied to the following example: + +```c# +public class Calculator +{ + [Log] + public double Divide(int a, int b) { return a / b; } + + [Log] + public void IntegerDivide(int a, int b, out int quotient, out int remainder) + { + quotient = a / b; + remainder = a % b; + } +} +``` + +Metalama will generate the following code at compile time: + +```c# +using Microsoft.Extensions.Logging; + +public class Calculator +{ + [Log] + public double Divide(int a, int b) + { + var isTracingEnabled = this._logger?.IsEnabled(LogLevel.Trace) == true; + if (isTracingEnabled) + { + LoggerExtensions.LogTrace(this._logger, "Calculator.Divide(int, int): Executing."); + } + + try + { + double result = a / b; + if (isTracingEnabled) + { + LoggerExtensions.LogTrace(this._logger, "Calculator.Divide(int, int): Completed."); + } + + return result; + } + catch (Exception e) when (this._logger?.IsEnabled(LogLevel.Warning) == true) + { + LoggerExtensions.LogWarning(this._logger, $"Calculator.Divide(int, int): {e.Message}."); + throw; + } + } + + [Log] + public void IntegerDivide(int a, int b, out int quotient, out int remainder) + { + var isTracingEnabled = this._logger?.IsEnabled(LogLevel.Trace) == true; + if (isTracingEnabled) + { + LoggerExtensions.LogTrace(this._logger, "Calculator.IntegerDivide(int, int, out int, out int): Executing."); + } + + try + { + quotient = a / b; + remainder = a % b; + if (isTracingEnabled) + { + LoggerExtensions.LogTrace(this._logger, "Calculator.IntegerDivide(int, int, out int, out int): Completed."); + } + } + catch (Exception e) when (this._logger?.IsEnabled(LogLevel.Warning) == true) + { + LoggerExtensions.LogWarning(this._logger, $"Calculator.IntegerDivide(int, int, out int, out int): {e.Message}."); + throw; + } + } + + private ILogger _logger; + + public Calculator(ILogger? logger = default) + { + this._logger = logger; + } +} +``` + +Note how Metalama automatically adds the necessary constructor to inject the `ILogger` dependency. + +As you can see, using dependency injection with Metalama is straightforward. For more details, refer to the [conceptual documentation](https://doc.metalama.net/conceptual/aspects/dependency-injection). + diff --git a/metalama-email-course/105-validate-references.md b/metalama-email-course/105-validate-references.md new file mode 100644 index 0000000..81f8f7c --- /dev/null +++ b/metalama-email-course/105-validate-references.md @@ -0,0 +1,96 @@ +--- +subject: "Validating Usages/References" +--- + +In previous emails, we focused on code generation and aspect-oriented programming. Now, it's time to revisit Metalama's second pillar: architecture validation. + +To collaborate effectively as a team, developers must follow specific rules and conventions. But how can these rules and conventions be enforced? In a small team working in the same office, this might be done informally through word of mouth or code comments like **DO NOT USE IN PRODUCTION CODE!**. These rules would then need to be checked during code reviews. Unfortunately, this approach is both time-consuming and error-prone. + +Wouldn't it be more efficient if developers were warned about errors or violations as they write code? + +The `Metalama.Extensions.Architecture` package provides several pre-made custom attributes and compile-time APIs to enforce common conventions that teams may want to follow. Additionally, you can create custom attributes or compile-time APIs to enforce rules specific to your team's needs. + +{: .note } +Architecture validation features, including the `Metalama.Extensions.Architecture` package, are available exclusively in Metalama Professional. These features are not included in the open-source version of Metalama. Our next email will focus on open-source features and use cases. + +Enforcing rules and conventions in this way allows you to: + +- Eliminate the need for a written set of rules that everyone must reference. +- Provide immediate feedback to developers directly within the IDE. +- Streamline code reviews by focusing solely on the code itself. +- Simplify the codebase by ensuring adherence to consistent rules. + +Let's explore a couple of examples. + +## Verifying usage with custom attributes + +In the first example, we'll address a common scenario where certain constructors of a class should only be used for testing. Metalama provides the [CanOnlyBeUsedFrom](https://doc.postsharp.net/etalama/api/metalama-extensions-architecture-aspects-canonlybeusedfromattribute) attribute for this purpose. + +Suppose we have a constructor that slightly modifies an object's behavior to make it more testable. We want to ensure this constructor is used only in tests. + +```c# +using Metalama.Extensions.Architecture.Aspects; + +public class OrderShipping +{ + private bool isTest; + + public OrderShipping() + { + } + + [CanOnlyBeUsedFrom(Namespaces = new[] {"**.Tests"})] + public OrderShipping(bool isTest) + { + // Used to trigger specific test configuration + this.isTest = isTest; + } +} +``` + +If we attempt to create a new `OrderShipping` instance in a namespace that doesn't end with `Tests`, a warning will appear. + +![](images/ValidatingTestWarning.jpg) + +![](images/ValidationWarning.jpg) + +However, if the constructor is called correctly from an allowed namespace, no warning will appear. + +![](images/ValidatingTestsNoWarning.jpg) + +## Verifying whole sets of types or members using fabrics + +In the previous example, we had to add a custom attribute to all types or members we wanted to control. If the same rule applied to many declarations, adding a custom attribute to each one could become tedious and counterproductive to Metalama's goal of reducing redundant code. + +This is where fabrics come in. + +As discussed in a previous email, fabrics are compile-time classes that execute within the compiler or IDE. One of their use cases is adding validation rules. + +Consider a second example. Suppose we have a project with many components, each implemented in its own namespace and consisting of several classes. There are so many components that we don't want to place each in its own project. + +However, we still want to isolate components from each other. Specifically, we want `internal` members of each namespace to be visible only within that namespace. Only `public` members should be accessible outside their home namespace. + +Additionally, we want `internal` components to be accessible from any test namespace. + +With Metalama, you can validate each namespace by adding the following fabric type: + +```cs +namespace MyComponent +{ + internal class Fabric : NamespaceFabric + { + public override void AmendNamespace(INamespaceAmender amender) + { + amender.InternalsCanOnlyBeUsedFrom(from => + from.CurrentNamespace().Or(or => or.Type("**.Tests.**"))); + } + } +} +``` + +Now, if foreign code tries to access an internal API of the `MyComponent` namespace, a warning will be reported. + +## Summary + +We've explored two examples of how to validate code using pre-built Metalama aspects. To learn more, refer to the documentation [here](https://doc.metalama.net/conceptual/architecture/usage), [here](https://doc.metalama.net/conceptual/architecture/naming-conventions), and [here](https://doc.metalama.net/conceptual/architecture/internal-only-implement). + diff --git a/metalama-email-course/110-creating-aspects-multi-kind.md b/metalama-email-course/110-creating-aspects-multi-kind.md new file mode 100644 index 0000000..20f2b66 --- /dev/null +++ b/metalama-email-course/110-creating-aspects-multi-kind.md @@ -0,0 +1,219 @@ +--- +subject: "Creating Custom Aspects: Multi-targeting" +--- + +After reading our introduction to creating custom aspects, you might think you need to create separate aspects for each target (field, property, method, or type). However, this is not the case. You can create a single aspect class that targets multiple elements, but it requires a slightly different approach. + +To illustrate this, let's consider a simple example of a logging aspect that can be applied to either a method or a property. + +The basic signature of the aspect looks like this: + +```c# +using Metalama.Framework.Aspects; +using Metalama.Framework.Code; + +namespace CreatingAspects.SimpleLogs +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] + public class LogAttribute : Attribute, IAspect, IAspect + { + } +} +``` + +In this example, we inherit directly from `System.Attribute` and implement Metalama's `IAspect` interface for both methods and fields or properties. The `[AttributeUsage]` attribute explicitly specifies where the attribute can be applied. + +To implement the interfaces, we need to add the following methods: + +```c# +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] +public class LogAttribute : Attribute, IAspect, IAspect +{ + public void BuildAspect(IAspectBuilder builder) + { + } + + public void BuildAspect(IAspectBuilder builder) + { + } +} +``` + +The `BuildAspect` methods are the _entry points_ of the aspect. The Metalama framework invokes one of these methods for each declaration to which the aspect is applied. The `BuildAspect` method is executed at _compile time_. + +Next, we need to add two _templates_: one for a method and one for a property. Templates must be annotated with the `[Template]` attribute. As you know, templates can combine compile-time and run-time code. + +Finally, we must edit the `BuildAspect` to instruct that Metalama must override the target method or property with the given template. This is done by calling the `builder.Advice.Override(target, template)` method. + +If you look at the source code of the [OverrideMethodAspect](https://github.com/postsharp/Metalama.Framework/blob/HEAD/Metalama.Framework/Aspects/OverrideMethodAspect.cs +), which should already be familiar to you, you will find that this is exactly what this class is doing for methods, except that the template method is abstract. + +Once the `BuildAspect` and template methods have been fleshed out, they should look like this: + +```c# +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] +public class LogAttribute : Attribute, IAspect, IAspect +{ + + public void BuildAspect(IAspectBuilder builder) + { + builder.Override(nameof(this.OverrideMethod)); + } + + public void BuildAspect(IAspectBuilder builder) + { + builder.Override(nameof(this.OverrideProperty)); + } + + [Template] + public dynamic? OverrideMethod() + { + var methodName = $"{meta.Target.Type}.{meta.Target.Method.Name}"; + try + { + Console.WriteLine($"You have entered {methodName}"); + return meta.Proceed(); + } + catch(Exception ex) + { + Console.WriteLine($"An error was encountered in {methodName}"); + return null; + } + } + + [Template] + public dynamic? OverrideProperty + { + get + { + var result = meta.Proceed(); + Console.WriteLine( + $"The value of {meta.Target.Type}.{meta.Target.Property.Name} is: {meta.Target.Property.Type} = {meta.Target.Property.Value}"); + return result; + } + + set + { + Console.WriteLine( + $"The old value of {meta.Target.Type} was: {meta.Target.Property.Type} = {meta.Target.Property.Value}"); + + meta.Proceed(); + + Console.WriteLine( + $"The new value of {meta.Target.Type} is: {meta.Target.Property.Type} = {meta.Target.Property.Value}"); + } + } +} +``` + +Now, logging can be applied to a simple `Order` class: + +```c# +namespace CreatingAspects.SimpleLogs +{ + internal class Order + { + public Order() + { + } + + [Log] + public void GenerateOrderNumber() + { + Random random = new Random(); + + int minValue = 1; + int maxValue = 100; + + OrderNumber = random.Next(minValue, maxValue); + } + + [Log] + public int OrderNumber { get; set; } + + } +} +``` + +After compiling, the code would look like this: + +```c# +namespace CreatingAspects.SimpleLogs +{ + internal class Order + { + public Order() + { + } + + [Log] + private void GenerateOrderNumber() + { + try + { + Console.WriteLine("You have entered Order.GenerateOrderNumber"); + Random random = new Random(); + + int minValue = 1; + int maxValue = 100; + + OrderNumber = random.Next(minValue, maxValue); + + return; + } + catch (Exception ex) + { + Console.WriteLine("An error was encountered in Order.GenerateOrderNumber"); + return; + } + } + + + private int _orderNumber; + [Log] + public int OrderNumber + { + get + { + var result = this._orderNumber; + Console.WriteLine($"The value of Order.OrderNumber is: int = {this._orderNumber}"); + return result; + + } + set + { + Console.WriteLine($"The old value of Order was: int = {this._orderNumber}"); + this._orderNumber = value; + Console.WriteLine($"The new value of Order is: int = {this._orderNumber}"); + + } + } + } +} +``` + +When the following code is run in a console application: + +```c# +namespace CreatingAspects.SimpleLogs +{ + internal class Program + { + static void Main(string[] args) + { + Order order = new Order(); + order.GenerateOrderNumber(); + } + } +} +``` + +It will produce the following output: + +```text +You have entered Order.GenerateOrderNumber +The old value of Order was: int = 0 +The new value of Order is: int = 59 +``` + +As you can see, Metalama is more powerful than it may initially appear. Classes such as `OverrideMethodAspect` serve as API sugar. For Metalama, what really matters are the `IAspect` interface and the operations performed by the `IAspect.BuildAspect` method. This method can add advice to the target, such as overriding a member with a template, implementing a new interface, or adding a new member. They can also report warnings or errors, suggest code fixes, validate references, and add chil aspects. You can discover this in the documentation of the [IAspectBuilder](https://doc.metalama.net/api/metalama-framework-aspects-iaspectbuilder) class. diff --git a/metalama-email-course/120-custom-notifiypropertychanged.md b/metalama-email-course/120-custom-notifiypropertychanged.md new file mode 100644 index 0000000..1a0325a --- /dev/null +++ b/metalama-email-course/120-custom-notifiypropertychanged.md @@ -0,0 +1,105 @@ +--- +subject: "Automating INotifyPropertyChanged on Your Terms" +--- + +In the previous emails, you learned how to create simple aspects by deriving from `OverrideMethodAspect`. Then, you explored more complex aspects by implementing the `IAspect` interface and its `BuildAspect` method. Today, we will delve into creating aspects that apply _multiple_ modifications to the target code, i.e., provide several pieces of advice. To illustrate this, we will implement the `INotifyPropertyChanged` interface, which requires three distinct operations on the target type and its properties. + +## Implementing `INotifyPropertyChanged` + +Application user interfaces are designed to respond almost instantaneously to user input. This is achieved through UIs built around data-bound controls in architectures that implement patterns such as MVVM (Model-View-ViewModel). Simply put, the UI updates when properties in the underlying data models change, triggering the `PropertyChanged` event. This behavior is encapsulated in the `INotifyPropertyChanged` interface. This pattern is widely adopted due to its ability to reuse data models across different views. + +However, using this interface has a significant drawback: it requires a large amount of repetitive boilerplate code. Since this code is not generated automatically, it is easy to unintentionally omit parts of it. + +The .NET class library already includes an `INotifyPropertyChanged` interface, so why not just use that? The drawback of this approach is illustrated below. + +![](images/notifypropertychanged1.gif) + +The standard Visual Studio _implement interface_ code fix does very little. You still need to adjust the properties to raise the event, and the event itself must be handled manually. + +By using Metalama to implement `INotifyPropertyChanged`, all the additional code required to make this work is handled automatically. You will need to create an aspect to achieve this, but fortunately, there is an excellent example of such an aspect in the [Metalama Documentation](https://doc.metalama.net/examples/notifypropertychanged). + +```c# +using Metalama.Framework.Aspects; +using Metalama.Framework.Code; +using System.ComponentModel; + +[Inheritable] +internal class NotifyPropertyChangedAttribute : TypeAspect +{ + public override void BuildAspect(IAspectBuilder builder) + { + builder.ImplementInterface(typeof(INotifyPropertyChanged), OverrideStrategy.Ignore); + + foreach (var property in builder.Target.Properties.Where(p => + !p.IsAbstract && p.Writeability == Writeability.All)) + { + builder.With(property).OverrideAccessors(null, nameof(this.OverridePropertySetter)); + } + } + + [InterfaceMember] + public event PropertyChangedEventHandler? PropertyChanged; + + [Introduce(WhenExists = OverrideStrategy.Ignore)] + protected void OnPropertyChanged(string name) => + this.PropertyChanged?.Invoke(meta.This, new PropertyChangedEventArgs(name)); + + [Template] + private dynamic OverridePropertySetter(dynamic value) + { + if (value != meta.Target.Property.Value) + { + meta.Proceed(); + this.OnPropertyChanged(meta.Target.Property.Name); + } + + return value; + } +} +``` + +If you examine the implementation of `BuildAspect`, you'll notice the following: + +1. It first implements the `INotifyPropertyChanged` interface by calling `builder.Advice.ImplementInterface`. The members of the `INotifyPropertyChanged` interface must be implemented in the aspect class and annotated with the `[InterfaceMember]` attribute. +2. It then iterates through the writable properties, modifying their setters using `builder.Advice.OverrideAccessors` to apply the `OverridePropertySetter` template method. +3. Finally, it adds an `OnPropertyChanged` method to the target type using the `[Introduce]` advice attribute. + +With this aspect added to your project, implementing `INotifyPropertyChanged` becomes significantly simpler. + +![](images/notifypropertychanged2.gif) + +In this small and contrived sample class, Metalama successfully implemented the `INotifyPropertyChanged` interface, saving us from writing 50 additional lines of code. In a larger, real-world project, the reduction in repetitive boilerplate code would be even more substantial. + +## Ready-Made Aspect + +As demonstrated, automating the implementation of `INotifyPropertyChanged` does not need to be overly complex. However, the aspects we created only handle the most basic cases. For example, we ignored scenarios involving computed properties that depend on the values of multiple other properties. While it is possible to enhance our aspect to handle such cases, doing so would increase its complexity. + +Fortunately, Metalama already provides a fully featured solution in the [Metalama.Patterns.Observable](https://doc.metalama.net/patterns/observability) package. + +The `[Observable]` aspect supports the following scenarios: +- Automatic properties (as handled by our basic aspect). +- Field-backed and explicitly implemented properties. +- Child objects. +- Child objects in derived types. + +For instance, consider the following class, which includes properties dependent on child objects. The `[Observable]` aspect analyzes the code and constructs a dependency graph of properties and fields. This approach allows your code to remain clean and maintainable: + +```csharp +[Observable] +public sealed class PersonViewModel +{ + public Person Person { get; set; } + + public string? FirstName => this.Person.FirstName; + + public string? LastName => this.Person.LastName; + + public string FullName => $"{this.FirstName} {this.LastName}"; +} +``` + +For more details about the `[Observable]` aspect, refer to its [documentation](https://doc.metalama.net/patterns/observability). + +## Conclusion + +Metalama provides you with complete control over code generation, enabling you to automate almost any pattern. For simple patterns, aspects are generally straightforward to implement. You will rarely encounter a scenario that Metalama cannot handle. However, for more complex patterns, it is worth checking whether a prebuilt aspect is already available. We publish these aspects on the [Metalama Marketplace](https://metalama.net/marketplace). \ No newline at end of file diff --git a/metalama-email-course/130-creating-aspects-inheritable.md b/metalama-email-course/130-creating-aspects-inheritable.md new file mode 100644 index 0000000..277b01a --- /dev/null +++ b/metalama-email-course/130-creating-aspects-inheritable.md @@ -0,0 +1,128 @@ +--- +subject: "Automatically Adding Aspects to Derived Types: Aspect Inheritance" +--- + +In the previous email, we explored how Metalama simplifies the implementation of `INotifyPropertyChanged` compared to relying solely on IntelliSense. To achieve this, we created a specific Metalama aspect. You might have missed it when we first introduced it, but this aspect was decorated with the `[Inheritable]` attribute. + +```c# +[Inheritable] +internal class NotifyPropertyChangedAttribute : TypeAspect +{ + // Aspect code here +} +``` + +By using this attribute, we ensured that the aspect could be inherited by classes derived from a class to which the `[NotifyPropertyChanged]` attribute had been applied. + +This allows us to create a very simple base class: + +```c# +[NotifyPropertyChanged] +public abstract partial class NotifyChangedBase +{ +} +``` + +This base class can then be utilized as shown below: + +![](images/us5.jpg) + +As you can see, the derived classes now have aspects applied to them. If we invoke the 'Show Metalama Diff' tool, we will observe the following: + +```c# +public partial class Customer : NotifyChangedBase +{ + private string? _address; + public string? Address + { + get + { + return this._address; + } + set + { + if (value != this._address) + { + this._address = value; + this.OnPropertyChanged("Address"); + } + } + } + + private string? _customerName; + public string? CustomerName + { + get + { + return this._customerName; + } + set + { + if (value != this._customerName) + { + this._customerName = value; + this.OnPropertyChanged("CustomerName"); + } + } + } +} + +public partial class Order : NotifyChangedBase +{ + private DateTime _orderDate; + public DateTime OrderDate + { + get + { + return this._orderDate; + } + set + { + if (value != this._orderDate) + { + this._orderDate = value; + this.OnPropertyChanged("OrderDate"); + } + } + } + + private DateTime _requiredBy; + public DateTime RequiredBy + { + get + { + return this._requiredBy; + } + set + { + if (value != this._requiredBy) + { + this._requiredBy = value; + this.OnPropertyChanged("RequiredBy"); + } + } + } +} +``` + +The base class contains the following implementation: + +```c# +using System.ComponentModel; + +[NotifyPropertyChanged] +public abstract partial class NotifyChangedBase : INotifyPropertyChanged +{ + protected void OnPropertyChanged(string name) + { + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } + + public event PropertyChangedEventHandler? PropertyChanged; +} +``` + +{. note } +When using the `[Inheritable]` aspect, careful consideration should be given to potential conflicts in derived classes if the aspect you wish to apply has already been applied. Specifically, you must pay attention to the `OverrideStrategy` parameters and properties (also named `WhenExists`). + +This approach keeps your codebase clean and uncluttered while maintaining clear intent. At compile time, everything needed to implement `INotifyPropertyChanged` is applied correctly. diff --git a/metalama-email-course/140-custom-validation-predicate.md b/metalama-email-course/140-custom-validation-predicate.md new file mode 100644 index 0000000..52da126 --- /dev/null +++ b/metalama-email-course/140-custom-validation-predicate.md @@ -0,0 +1,115 @@ +--- +subject: "Validating Architecture on Your Terms: Custom Predicates" +--- + +In previous discussions, we explored how Metalama provides several pre-built fabric extension methods to help validate the architecture of your codebase. However, there may be scenarios where these pre-built methods do not fully meet your requirements, necessitating custom validation. + +Metalama offers various methods for architecture validation, such as `CanOnlyBeUsedFrom`, `CannotBeUsedFrom`, `InternalsCanOnlyBeUsedFrom`, and `InternalsCannotBeUsedFrom`. These methods accept predicates like `Assembly(name)`, `CurrentNamespace()`, `Namespace(name)`, `NamespaceOf(type)`, `Type(type)`, or `HasFamilyAccess`, all exposed as extension methods of the [ReferencePredicateBuilder](https://doc.metalama.net/api/metalama-extensions-architecture-predicates-referencepredicatebuilder) class. Before creating your own validation methods, consider whether you can use these methods with a custom predicate. This is the approach we will explore in this article. + +## 1. Create a Predicate Class + +The first step is to create a class derived from the `ReferencePredicate` abstract class and implement its `IsMatch` method. While implementing a class instead of supplying a delegate may seem cumbersome at first, it is necessary because predicates must be serializable to validate references from other projects. + +We recommend keeping this predicate class internal. + +```c# +using Metalama.Extensions.Architecture.Fabrics; +using Metalama.Extensions.Architecture.Predicates; +using Metalama.Framework.Aspects; +using Metalama.Framework.Code; +using Metalama.Framework.Fabrics; +using Metalama.Framework.Validation; + +/// +/// A method name predicate. +/// +/// +/// This class defines the predicate. It checks method names to determine if, +/// in this case, they end with a specific word or phrase. +/// +internal class MethodNamePredicate : ReferencePredicate +{ + private readonly string _suffix; + + public MethodNamePredicate(ReferencePredicateBuilder? builder, string suffix) : base(builder) + { + this._suffix = suffix; + } + + public override bool IsMatch(in ReferenceValidationContext context) + { + return context.ReferencingDeclaration is IMethod method && + method.Name.EndsWith(this._suffix, StringComparison.Ordinal); + } +} +``` + +## 2. Create an Extension Method + +The next step is to create a public extension method for your predicate. + +```cs +/// +/// A class to expose your custom extensions. +/// +/// +/// This class serves as an API for your extensions. +/// +[CompileTime] +public static class Extensions +{ + public static ReferencePredicate MethodNameEndsWith(this ReferencePredicateBuilder? builder, string suffix) + => new MethodNamePredicate(builder, suffix); +} +``` + +## 3. Use Your Extension Method in a Fabric + +You can now use your predicate method in any fabric. In this example, let's assume a proud `CoffeeMachine` wants to be called only from methods whose names end with `Politely`. + +```cs +/// +/// A project fabric, i.e., a compile-time entry point for your project. +/// +internal class Fabric : ProjectFabric +{ + public override void AmendProject(IProjectAmender amender) + { + // Validate that methods within a certain type (in this case, CoffeeMachine) + // can only be called from methods whose names end with "Politely". + amender + .SelectTypes(typeof(CoffeeMachine)) + .CanOnlyBeUsedFrom(r => r.MethodNameEndsWith("Politely")); + } +} +``` + +```cs +// A proud coffee machine. This is the class whose method(s) we wish to validate. +internal static class CoffeeMachine +{ + public static void TurnOn() + { + } +} + +// A test class to verify the new predicate. +internal class Bar +{ + public static void OrderCoffee() + { + // This call to CoffeeMachine is reported because the method is not polite enough. + CoffeeMachine.TurnOn(); + } + + public static void OrderCoffeePolitely() + { + // This call to CoffeeMachine is accepted because the method is polite. + CoffeeMachine.TurnOn(); + } +} +``` + +The functionality of this code extension is demonstrated in the GIF below. + +![](images/refpredicate.gif) diff --git a/metalama-email-course/images/ValidatingTestWarning.jpg b/metalama-email-course/images/ValidatingTestWarning.jpg new file mode 100644 index 0000000..f9edfc5 Binary files /dev/null and b/metalama-email-course/images/ValidatingTestWarning.jpg differ diff --git a/metalama-email-course/images/ValidatingTestsNoWarning.jpg b/metalama-email-course/images/ValidatingTestsNoWarning.jpg new file mode 100644 index 0000000..1884dd1 Binary files /dev/null and b/metalama-email-course/images/ValidatingTestsNoWarning.jpg differ diff --git a/metalama-email-course/images/ValidationWarning.jpg b/metalama-email-course/images/ValidationWarning.jpg new file mode 100644 index 0000000..1fd3697 Binary files /dev/null and b/metalama-email-course/images/ValidationWarning.jpg differ diff --git a/metalama-email-course/images/aspect-inheritance.jpg b/metalama-email-course/images/aspect-inheritance.jpg new file mode 100644 index 0000000..6ffd4a0 Binary files /dev/null and b/metalama-email-course/images/aspect-inheritance.jpg differ diff --git a/metalama-email-course/images/aspectViewer.png b/metalama-email-course/images/aspectViewer.png new file mode 100644 index 0000000..2187470 Binary files /dev/null and b/metalama-email-course/images/aspectViewer.png differ diff --git a/metalama-email-course/images/aspectViewer1.png b/metalama-email-course/images/aspectViewer1.png new file mode 100644 index 0000000..3c8b304 Binary files /dev/null and b/metalama-email-course/images/aspectViewer1.png differ diff --git a/metalama-email-course/images/attribute-namingconvention.png b/metalama-email-course/images/attribute-namingconvention.png new file mode 100644 index 0000000..43e1d7d Binary files /dev/null and b/metalama-email-course/images/attribute-namingconvention.png differ diff --git a/metalama-email-course/images/creating-aspects-inheritable.gif b/metalama-email-course/images/creating-aspects-inheritable.gif new file mode 100644 index 0000000..a991aae Binary files /dev/null and b/metalama-email-course/images/creating-aspects-inheritable.gif differ diff --git a/metalama-email-course/images/diagnostics.gif b/metalama-email-course/images/diagnostics.gif new file mode 100644 index 0000000..56af271 Binary files /dev/null and b/metalama-email-course/images/diagnostics.gif differ diff --git a/metalama-email-course/images/experimental.gif b/metalama-email-course/images/experimental.gif new file mode 100644 index 0000000..cb2512a Binary files /dev/null and b/metalama-email-course/images/experimental.gif differ diff --git a/metalama-email-course/images/fabric1.jpg b/metalama-email-course/images/fabric1.jpg new file mode 100644 index 0000000..1b04d88 Binary files /dev/null and b/metalama-email-course/images/fabric1.jpg differ diff --git a/metalama-email-course/images/fabric2.jpg b/metalama-email-course/images/fabric2.jpg new file mode 100644 index 0000000..fbe5d42 Binary files /dev/null and b/metalama-email-course/images/fabric2.jpg differ diff --git a/metalama-email-course/images/naming-conventions-1.gif b/metalama-email-course/images/naming-conventions-1.gif new file mode 100644 index 0000000..249e60a Binary files /dev/null and b/metalama-email-course/images/naming-conventions-1.gif differ diff --git a/metalama-email-course/images/notifypropertychanged1.gif b/metalama-email-course/images/notifypropertychanged1.gif new file mode 100644 index 0000000..8b17947 Binary files /dev/null and b/metalama-email-course/images/notifypropertychanged1.gif differ diff --git a/metalama-email-course/images/notifypropertychanged2.gif b/metalama-email-course/images/notifypropertychanged2.gif new file mode 100644 index 0000000..1c91ad5 Binary files /dev/null and b/metalama-email-course/images/notifypropertychanged2.gif differ diff --git a/metalama-email-course/images/refpredicate.gif b/metalama-email-course/images/refpredicate.gif new file mode 100644 index 0000000..4bebc85 Binary files /dev/null and b/metalama-email-course/images/refpredicate.gif differ diff --git a/metalama-email-course/images/vsx2.gif b/metalama-email-course/images/vsx2.gif new file mode 100644 index 0000000..13a7eb7 Binary files /dev/null and b/metalama-email-course/images/vsx2.gif differ diff --git a/metalama-email-course/images/vsx3.gif b/metalama-email-course/images/vsx3.gif new file mode 100644 index 0000000..cc60eca Binary files /dev/null and b/metalama-email-course/images/vsx3.gif differ diff --git a/metalama-email-course/xx-aspect-inheritance.md b/metalama-email-course/xx-aspect-inheritance.md new file mode 100644 index 0000000..94ddc47 --- /dev/null +++ b/metalama-email-course/xx-aspect-inheritance.md @@ -0,0 +1,153 @@ +subject: "Using Metalama: Inheritance (via an Interface)" +--- +--- + +In our previous discussions about common tasks that can be simplified with Metalama, we demonstrated how adding simple attributes to your code can eliminate the need for writing large amounts of repetitive boilerplate code. This makes your code more compact and clarifies its purpose to any reader. + +You might wonder if adding attributes to the code base is less cumbersome than writing the actual boilerplate code that would otherwise be required. Does this mean you need to add these attributes to every piece of code where a task needs to be performed? + +The answer is no, thanks to Metalama's support for inheritance. Let's consider a simple example. + +In a typical Line of Business application, there might be classes representing customers, suppliers, and employees. Although these classes represent different aspects of the business, they likely share some commonalities, such as an email address, a phone number, and some indication of their importance to the business. + +This could lead us to introduce an interface to our application (which is, in itself, a type of contract) that could look like this: + +```c# +using Metalama.Patterns.Contracts; + +namespace UsingMetalama.Inheritance +{ + public interface IContact + { + + [Email] + string Email { get; set; } + + [Required] + bool? IsActive { get; set; } + + [Phone] + string PhoneNumber { get; set; } + + } +} +``` + +We could then create a Customer class that implements our IContact interface like this: + +```c# +namespace UsingMetalama.Inheritance +{ + public class Customer : IContact + { + + public string? CustomerName { get; set; } + + public string? Email { get; set; } + + public bool? IsActive { get; set; } + + public string? PhoneNumber { get; set; } + + + } +} +``` + +At this point, you might be wondering if the attributes we added to the interface have been carried forward. They have indeed, and a screenshot from the IDE itself proves the point. + +![](images/aspect-inheritance.jpg) + +Notice how each of the properties inherited from the interface indicates that there is an aspect associated with it. When the code itself is compiled, this will be the final result. + +> The Code lens feature is added by the Metalama Tools for Visual Studio Extension and is, by definition, only available when using Visual Studio. + +```c# +using Metalama.Patterns.Contracts; + +namespace UsingMetalama.Inheritance +{ + public class Customer : IContact + { + + public string? CustomerName { get; set; } + + + private string? _email; + + public string? Email + { + get + { + return this._email; + + + } + set + { + var regex = ContractHelpers.EmailRegex!; + if (value != null && !regex.IsMatch(value!)) + { + var regex_1 = regex; + throw new ArgumentException("The 'Email' property must be a valid email address.", "value"); + } + this._email = value; + + + } + } + + + private bool? _isActive; + + public bool? IsActive + { + get + { + return this._isActive; + + + } + set + { + if (value == null!) + { + throw new ArgumentNullException("value", "The 'IsActive' property is required."); + } + this._isActive = value; + + + } + } + + + private string? _phoneNumber; + + public string? PhoneNumber + { + get + { + return this._phoneNumber; + + + } + set + { + var regex = ContractHelpers.PhoneRegex!; + if (value != null && !regex.IsMatch(value!)) + { + var regex_1 = regex; + throw new ArgumentException("The 'PhoneNumber' property must be a valid phone number.", "value"); + } + this._phoneNumber = value; + + + } + } + + + } +} +``` + +Metalama automatically propagated contract attributes from the base class to the derived class. The `Customer` class, and indeed any other class that implements the `IContact` interface, will remain compact, clean, and easy to read. However, at compile time, the required functionality will be added. This not only saves you from writing a substantial amount of boilerplate code, but also ensures that it's done consistently for you. diff --git a/metalama-email-course/xx-eligibility-and-diagnostics.md b/metalama-email-course/xx-eligibility-and-diagnostics.md new file mode 100644 index 0000000..9ab5d27 --- /dev/null +++ b/metalama-email-course/xx-eligibility-and-diagnostics.md @@ -0,0 +1,314 @@ +subject: "Creating Aspects: Eligibility and Diagnostics" +--- + +{% raw %} + +> TODO: The example is constrived. A caching example woud be better. + +We've explored how Metalama can be used to create sophisticated custom aspects, but we haven't yet discussed how to ensure they're not used inappropriately. + +Let's revisit the version of the logging aspect that had a dependency on `ILogger`. We saw how Metalama introduced an appropriate constructor at compile time. However, static classes can't have constructors, and while Metalama will produce an error message if you try to manually add the Log Aspect to a method in a static class, the reality with something like logging—which you'd probably want to apply comprehensively—is that you'd use a fabric to apply the attribute. This requires a way to ensure that the aspect is only applied where it's appropriate. + +Below, we have a revised version of our Log aspect. The functionality remains the same, but we've added some logic to ensure that it's only applied where it's safe to do so. + +```c# +using Metalama.Extensions.DependencyInjection; +using Metalama.Framework.Advising; +using Metalama.Framework.Aspects; +using Metalama.Framework.Code; +using Metalama.Framework.CodeFixes; +using Metalama.Framework.Diagnostics; +using Metalama.Framework.Eligibility; +using Microsoft.Extensions.Logging; + +[AttributeUsage(AttributeTargets.Method)] +public class LogAttribute : Attribute, IAspect +{ + [IntroduceDependency] + private readonly ILogger _logger; + + private static DiagnosticDefinition vtl105Error = new( + "VTL105", + Severity.Error, + "This class has already been marked as not requiring logging. Remove the [Log] Aspect"); + + public void BuildAspect(IAspectBuilder builder) + { + if(builder.Target.DeclaringType.Attributes.OfAttributeType(typeof(NoLogAttribute)).Any()) + { + if(builder.Target.Attributes.OfAttributeType(typeof(LogAttribute)).Any()) + { + builder.Diagnostics.Report(vtl105Error.WithArguments(builder.Target)); + + builder.Diagnostics.Suggest( + CodeFixFactory.RemoveAttributes(builder.Target, typeof(LogAttribute), "Remove Aspect | Log")); + } + builder.SkipAspect(); + } else + { + if(!(builder.Target.Attributes.OfAttributeType(typeof(NoLogAttribute)).Any())) + { + builder.Advice.Override(builder.Target, nameof(this.OverrideMethod)); + } + else + { + builder.SkipAspect(); + } + } + } + + public void BuildEligibility(IEligibilityBuilder builder) + { + builder.AddRule(EligibilityRuleFactory.GetAdviceEligibilityRule(AdviceKind.OverrideMethod)); + builder.DeclaringType().MustNotHaveAspectOfType(typeof(NoLogAttribute)); + builder.MustNotBeStatic(); + builder.MustNotHaveAspectOfType(typeof(NoLogAttribute)); + } + + [Template] + public dynamic? OverrideMethod() + { + // Add the code from the previous Log aspect here + } + + // Add the InterpolatedStringBuilder here +} + + + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method )] +public sealed class NoLogAttribute : Attribute +{ +} + +``` + +This aspect will be applied with a Fabric, which simplifies its widespread application. That Fabric is reproduced below. + +```c# +using Metalama.Framework.Fabrics; + +public class ProjectLoggerApplication : ProjectFabric +{ + public override void AmendProject(IProjectAmender amender) + { + amender.Outbound + .SelectMany(compilation => compilation.AllTypes) + .Where(type => !type.IsStatic || type.Attributes.OfAttributeType(typeof(NoLogAttribute)).Any()) + .SelectMany(type => type.Methods) + .Where(method => method.Name != "ToString") + .AddAspectIfEligible(); + } +} + +``` + +For the sake of brevity, we've omitted the bulk of the code from our previous example to focus on what's been added. + +Let's start with the `BuildAspect` method. This method adds some [diagnostics and code fixes](https://doc.metalama.net/conceptual/aspects/diagnostics) to the IDE to catch instances when you might manually add the `[Log]` attribute in an ineligible location—specifically when a class has been marked as one that should not have any members logged. + +> This implementation uses a simple `[NoLog]` attribute (reproduced above) that indicates that either a class or method should not have logging applied to it. + +Initially, a check is made for the presence of the `{NoLog]` on the class itself. If the class shouldn't be logged, then there's no point in adding logging. However, a diagnostic is there to catch occasions when the `[Log]` attribute might be manually added, and a codefix is available to fix it. + +If the class itself can have logging, then the individual methods are checked to see if they've been decorated with the `[NoLog]` attribute. If it's not present, the `OverrideMethod` template is called to add the aspect. + +Within the Fabric, you'll notice that we use `AddAspectIfEligible<>()`. The eligibility is checked in the preceding lines, ensuring that the type isn't static (because Dependency Injection requires a constructor), that we're looking at a method, and that the method is not a ToString() implementation (to avoid potential recursion). + +The `BuildEligibility` method ensures that users of your `[Log]` attribute can only apply it in areas where you've designed it to be applied. See the documentation [here](https://doc.metalama.net/conceptual/aspects/eligibility). + +Let's now look at how the following class could be affected. + +```c# +public partial class Calculator +{ + public double Divide(int a, int b) { return a / b; } + + public void IntegerDivide(int a, int b, out int quotient, out int remainder) + { + quotient = a / b; + remainder = a % b; + } +} + +``` + +In this instance, everything should be logged, and indeed it is, with the Fabric applying the log aspect to each method. + + +```c# +using Microsoft.Extensions.Logging; + +public partial class Calculator +{ + + public double Divide(int a, int b) + { + var isTracingEnabled = this._logger.IsEnabled(LogLevel.Trace); + + if (isTracingEnabled) + { + LoggerExtensions.LogTrace(this._logger, $"Calculator.Divide(a = {{{a}}}, b = {{{b}}}) started."); + } + + try + { + double result; + result = a / b; + + if (isTracingEnabled) + { + LoggerExtensions.LogTrace(this._logger, $"Calculator.Divide(a = {{{a}}}, b = {{{b}}}) returned {result}."); + } + + return (double)result; + } + catch (Exception e) when (this._logger.IsEnabled(LogLevel.Warning)) + { + LoggerExtensions.LogWarning(this._logger, $"Calculator.Divide(a = {{{a}}}, b = {{{b}}}) failed: {e.Message}"); + throw; + } + } + + public void IntegerDivide(int a, int b, out int quotient, out int remainder) + { + var isTracingEnabled = this._logger.IsEnabled(LogLevel.Trace); + if (isTracingEnabled) + { + LoggerExtensions.LogTrace(this._logger, $"Calculator.IntegerDivide(a = {{{a}}}, b = {{{b}}}, quotient = , remainder = ) started."); + } + + try + { + quotient = a / b; + remainder = a % b; + + object result = null; + if (isTracingEnabled) + { + LoggerExtensions.LogTrace(this._logger, $"Calculator.IntegerDivide(a = {{{a}}}, b = {{{b}}}, quotient = {{{quotient}}}, remainder = {{{remainder}}}) succeeded."); + } + + return; + } + catch (Exception e) when (this._logger.IsEnabled(LogLevel.Warning)) + { + LoggerExtensions.LogWarning(this._logger, $"Calculator.IntegerDivide(a = {{{a}}}, b = {{{b}}}, quotient = , remainder = ) failed: {e.Message}"); + throw; + } + } + + + private ILogger _logger; + + public Calculator (ILogger logger = default) + { + this._logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); + } +} +``` + +In the following case, no logs should be generated. + +```csharp +namespace CreatingAspects.Logging +{ + [NoLog] + public partial class Calculator + { + public double Divide(int a, int b) { return a / b; } + + public void IntegerDivide(int a, int b, out int quotient, out int remainder) + { + quotient = a / b; + remainder = a % b; + } + } +} +``` + +As demonstrated, no logs are generated. + +```csharp +[NoLog] +public partial class Calculator +{ + public double Divide(int a, int b) { return a / b; } + + public void IntegerDivide(int a, int b, out int quotient, out int remainder) + { + quotient = a / b; + remainder = a % b; + } +} +``` + +In the example below, logging should only be applied to the `IntegerDivide` method. + +```csharp +public partial class Calculator +{ + [NoLog] + public double Divide(int a, int b) { return a / b; } + + public void IntegerDivide(int a, int b, out int quotient, out int remainder) + { + quotient = a / b; + remainder = a % b; + } +} + +``` + +As expected, logging is only applied to the `IntegerDivide` method. + +```csharp +using Microsoft.Extensions.Logging; + +public partial class Calculator +{ + [NoLog] + public double Divide(int a, int b) { return a / b; } + + public void IntegerDivide(int a, int b, out int quotient, out int remainder) + { + var isTracingEnabled = this._logger.IsEnabled(LogLevel.Trace); + if (isTracingEnabled) + { + LoggerExtensions.LogTrace(this._logger, $"Calculator.IntegerDivide(a = {{{a}}}, b = {{{b}}}, quotient = , remainder = ) started."); + } + + try + { + quotient = a / b; + remainder = a % b; + + object result = null; + if (isTracingEnabled) + { + LoggerExtensions.LogTrace(this._logger, $"Calculator.IntegerDivide(a = {{{a}}}, b = {{{b}}}, quotient = {{{quotient}}}, remainder = {{{remainder}}}) succeeded."); + } + + return; + } + catch (Exception e) when (this._logger.IsEnabled(LogLevel.Warning)) + { + LoggerExtensions.LogWarning(this._logger, $"Calculator.IntegerDivide(a = {{{a}}}, b = {{{b}}}, quotient = , remainder = ) failed: {e.Message}"); + throw; + } + } +} + +``` + +In the final example, an error and a suggested code fix should be observed if an attempt is made to manually add the Log aspect to a class that should not have logging. + +![](images/diagnostics.gif) + +By leveraging the capabilities of Metalama, powerful custom aspects can be developed. + +For more information about Metalama, please visit our [website](https://www.postsharp.net/metalama). + +We also encourage you to join us on [Slack](https://www.postsharp.net/slack), where you can stay updated on our latest developments and get answers to your technical questions. + +{% endraw %} \ No newline at end of file diff --git a/metalama-email-course/xx-more-contracts.md b/metalama-email-course/xx-more-contracts.md new file mode 100644 index 0000000..2ecf6aa --- /dev/null +++ b/metalama-email-course/xx-more-contracts.md @@ -0,0 +1,166 @@ +subject: "Common Tasks: Input / Output Validation" +--- + +# Common Tasks: Input / Output Validation + +The phrase 'Garbage in, garbage out' is well-known among developers. Essentially, if the input to an application is flawed, it shouldn't be surprising if the output is also flawed. To prevent this, developers must ensure that both the input to their application's routines and the output from them meet acceptable criteria. + +Validation is a task that every developer must tackle at some point. The approach they adopt often serves as a good indicator of their overall development experience. + +Consider a basic requirement where a given string must fall within a certain number of characters. + +A novice or relatively inexperienced developer might create a simple checking method to fulfill the immediate requirement. + +```c# +bool ValidateString(string input) +{ + return input.length < 10 && input.length > 16; +} +``` + +If the requirement is for the string to be no less than 10 characters in length and no more than 16, then this simple validation will provide an answer to the basic question: 'Does this string fall within the defined character length?' However, it doesn't adequately handle failure. Over time, developers learn to handle this more effectively, but they still find themselves adopting different approaches depending on whether they are validating parameter inputs, properties, or results. + +With Metalama, tasks like these can be solved easily. It offers a package `Metalama.Patterns.Contracts` that provides a number of pre-made contracts for a wide range of scenarios, including the example discussed here. + +You could write code as follows (using a simple console application as an example), employing nothing more than a simple attribute: + +```c# +using Metalama.Patterns.Contracts; + +namespace CommonTasks +{ + internal class Program + { + + [StringLength(10, 16)] + private static string? password; + + static void Main(string[] args) + { + try + { + Console.WriteLine("Enter your Password: "); + password = Console.ReadLine(); + Console.WriteLine("Your password meets the basic length requirement."); + } catch(ArgumentException ex) + { + Console.WriteLine(ex.Message); + } + } + } +} +``` + +Metalama's StringLength aspect accepts either a maximum, or a minimum and maximum length as parameters and, in the event of a validation failure, throws a System.ArgumentException. + +At the point of compilation, it injects the necessary logic into your code. + +```c# +using Metalama.Patterns.Contracts; + +namespace CommonTasks +{ + internal class Program + { + private static string? _password1; + + + [StringLength(10, 16)] + private static string? password + { + get + { + return Program._password1; + + + } + set + { + if (value != null && (value!.Length < 10 || value.Length > 16)) + { + throw new ArgumentException($"The 'password' property must be a string with length between {10} and {16}.", "value"); + } + Program._password1 = value; + + + } + } + static void Main(string[] args) + { + try + { + Console.WriteLine("Enter your Password: "); + password = Console.ReadLine(); + Console.WriteLine("Your password meets the basic length requirement."); + } + catch (ArgumentException ex) + { + Console.WriteLine(ex.Message); + } + } + } +} +``` + +There are numerous benefits to using Metalama contracts for validation. They are named in a way that clearly indicates their purpose, and where appropriate, they accept parameters that provide flexibility in the rules being tested. When validation fails, it does so by throwing standard exceptions that are easy to handle. The real benefit, however, is that they can be used consistently to validate both inputs and outputs. + +In the two examples that follow, the task remains the same, but instead of validating a property, input parameters are validated in the first example, and the actual output in the second. In both cases, the code that Metalama adds at compilation is also shown. + +```c# +static string CreatePasswordValidatingInputs([StringLength(5,8)]string a, [StringLength(5, 8)] string b) +{ + return a + b; +} +``` + +Which at compile time becomes: + +```c# + static string CreatePasswordValidatingInputs([StringLength(5,8)]string a, [StringLength(5, 8)] string b) + { + if (b.Length < 5 || b.Length > 8) + { + throw new ArgumentException($"The 'b' parameter must be a string with length between {5} and {8}.", "b"); + } + if (a.Length < 5 || a.Length > 8) + { + throw new ArgumentException($"The 'a' parameter must be a string with length between {5} and {8}.", "a"); + } + return a + b; + + } +``` + +And for outputs: + +```c# + [return: StringLength(10,16)] + static string CreatePasswordValidatingOutput(string a, string b) + { + return a + b; + } +``` + +Which at compile time becomes: + +```c# + [return: StringLength(10,16)] + static string CreatePasswordValidatingOutput(string a, string b) + { + string returnValue; + returnValue = a + b; + + + if (returnValue.Length < 10 || returnValue.Length > 16) + { + throw new PostconditionViolationException($"The return value must be a string with length between {10} and {16}."); + } + return returnValue; + + } +``` + +The same contract is used in ostensibly the same way via an attribute for three quite different validation scenarios but produces consistent code at compilation time that the developer has not had to write by hand. + +> **Note: While the System.ComponentModel.DataAnnotations library includes a StringLength attribute, it does not offer the same versatility as that provided by Metalama.Patterns.Contracts, as it cannot be applied to return values and requires the developer to provide their own error message.** + diff --git a/misc/goat-review/architecture.md b/misc/goat-review/architecture.md new file mode 100644 index 0000000..01e5073 --- /dev/null +++ b/misc/goat-review/architecture.md @@ -0,0 +1,206 @@ +[In my previous article](https://goatreview.com/generating-repetitive-code-with-metalama/), I demonstrated how Metalama can generate boilerplate code during compilation, automating the repetitive yet necessary tasks. But Metalama doesn’t stop there. If Metalama were a goat, its second horn would be its ability to validate source code against architectural rules — ensuring that your code stays on track. This is the focus of today’s article. + +## Why Does Architecture Matter? + +Most non-trivial projects start with a phase where the team defines the application architecture. Software architecture is a broad concept. At the highest level, you have _solution architecture_, which defines the different applications and ways of communication. On a lower level, the _application architecture_ selects the frameworks, defines the base classes and interfaces, and designs the implementation patterns. Defining the application architecture is a creative and iterative process. While it can be done in a waterfall way using UML diagrams (and probably should in complex projects), the architecture will be refined over time during the first weeks of the project. + +Once the architecture is well-understood, it's important that it's _respected_. You can understand architecture as a set of _generative rules_, i.e., rules from which artifacts are built. This, by the way, is not unique to software architecture and applies to buildings and urbanism. Code is to programmers what bricks are to masons. + +Software architecture directly relates to software complexity. An important metric in software complexity is the number of rules _and exceptions_ it follows. The fewer rules and exceptions, the lower the complexity. + +To take an analogy in information theory, consider a compression algorithm like Brotli or LZMA. Their whole purpose is to reduce the _predictability_ of the input stream to its minimum. The output of this algorithm is reduced to the real informational complexity of the input stream. Of course, I'm not even remotely suggesting that your code should look like Brotli-encoded. What I'm suggesting is that it should have minimal informational complexity. Because, eventually, we have to "load" this informational complexity into our brains. And, if you think your brain has unlimited capacity, be certain that the cognitive abilities of your colleagues have some limits! + +To have minimal informational complexity in a codebase, and to make sure the codebase fits in your brain, you should strive not only for the lowest number of rules but also for the fewest exceptions to the rules, because both rules and exceptions count as pieces of information. + +At the end of the day, codebase complexity is the ultimate metric in software engineering. We are rarely limited by hardware resources. Most of the time, the limiting factor we software engineers have to deal with is our own limited cognitive capacity, both as individuals and collectively. How many _smart enough_ developers can you hire for your budget? The lower the codebase complexity, the larger the pool you can hire from. + +When code complexity is too high, productivity drops, and bugs surge. + +## What is Architecture Erosion? + +Most of the time, the output of the software architecture role is a set of texts and diagrams describing the rules, conventions, and patterns that we would like the codebase to follow. + +Because these texts and diagrams are not provided in executable form, rule violations can happen in source code, degrading code quality. To avoid rule violations, we perform code reviews, a manual process that sometimes happens days after the code has been written. First, in an attempt to streamline the merge process, benign violations are ignored. Then, the broken window syndrome happens, and more and more violations are accepted in the codebase. Progressively, rules are no longer followed. With turnover in the team, new team members may not even be trained in the original architecture. + +This process is called [architecture erosion](https://ieeexplore.ieee.org/document/9463012): the growing gap between the original architectural intention and its implementation in source code. + + +## How Can Metalama Help Avoid Architecture Erosion? + +As we have seen, one of the principal causes of architecture erosion is the lack of automated verification of the source code against the architecture, relying instead on the long feedback loop provided (in the best cases) by code reviews. + +Metalama allows you to [validate your architecture](https://doc.metalama.net/conceptual/architecture) both _in real-time_, straight from the IDE, and during your CI build. Therefore, the feedback loop is shortened from hours to seconds. Violations can be corrected immediately. As for the most important defects, they will generate an error and won't even pass the continuous integration build. + +Like the Provençal goats of the Luberon, your code must roam free but within well-defined limits, respecting the _terroir_ of your architecture. + +The [open-source](https://github.com/postsharp/Metalama.Extensions/tree/HEAD/src/Metalama.Extensions.Architecture) [Metalama.Extensions.Architecture](https://www.nuget.org/packages/Metalama.Extensions.Architecture) package offers several pre-made custom attributes and compile-time APIs that cover many common conventions teams might want to follow. + +Let's see two families of rules you can easily validate with Metalama: naming conventions and component dependencies. + +## Verifying Naming Conventions + +_Il faut appeler une chèvre une chèvre._ + +You’ve perhaps experienced how hard it can be to align everyone on the same [naming conventions](https://doc.metalama.net/conceptual/architecture/naming-conventions). With Metalama, you define rules and conventions using plain C#. They will be enforced both in real-time in the IDE and at compile time. + +For instance, assume you want every class implementing `ICheeseFactory` to have the `CheeseFactory` suffix. You can do this with a single attribute: [DerivedTypesMustRespectNamingConvention](https://doc.metalama.net/api/metalama-extensions-architecture-aspects-derivedtypesmustrespectnamingconventionattribute). + +```csharp +[DerivedTypesMustRespectNamingConvention( "*CheeseFactory" )] +public interface ICheeseFactory +{ + Cheese Create( string king, decimal quantity ); +} +``` + +If someone violates this rule, a warning will immediately be reported: + +``` +LAMA0903. The type ‘CheeseGenerator’ does not respect the naming convention set on the base class or interface ICheeseFactory. The type name should match the "*CheeseFactory" pattern. +``` + +The shorter the feedback loop is, the smoother the code reviews will go! Not to mention the frustration both sides avoided! + +For details regarding naming convention enforcement, please refer to the [Metalama documentation](https://doc.metalama.net/conceptual/architecture/naming-conventions). + +## Validating Dependencies + +Let's examine how to verify that components are _used_ as intended. + +Let's assume we have a constructor that slightly modifies the object's behavior to make it more testable. We want to ensure that this constructor is used only in tests. Metalama provides the [CanOnlyBeUsedFrom](https://doc.postsharp.net/etalama/api/metalama-extensions-architecture-aspects-canonlybeusedfromattribute) attribute for this purpose. + +```csharp +public class CheeseFactory +{ + private bool isTest; + + public CheeseFactory() + { + } + + [CanOnlyBeUsedFrom(Namespaces = new[] {"**.Tests"})] + public CheeseFactory(bool isTest) + { + // Used to trigger specific test configuration + this.isTest = isTest; + } +} +``` + +If we attempt to create a new `CheeseFactory` instance in a namespace that isn't suffixed by `Tests`, we will see a warning. + +What's important here is that we have a way to convey the _design intent_ we had when writing a piece of code. Many defects stem from the fact that the design intent of the initial author faded away. Thanks to meta-programming, you can make this design intent explicit and verified in real time. + +For details regarding usage validation, please refer to the [Metalama documentation](https://doc.metalama.net/conceptual/architecture/usage). + +## Fabrics + +In the previous examples, I have used custom attributes to express [architectural constraints](https://doc.metalama.net/conceptual/architecture/usage). However, this is not always the most convenient way to express architecture. + +Suppose we have a project composed of a large number of components. Each of these components is implemented in its own namespace and is made up of several classes. There are so many components that we don't want to have them each in their own project. + +However, we still want to isolate components from each other. Specifically, we want `internal` members of each namespace to be visible only within this namespace. Only `public` members should be accessible outside of its home namespace. + +Additionally, we want `internal` components to be accessible from any test namespace. + +With Metalama, you can validate each namespace by adding a [fabric](https://doc.metalama.net/conceptual/using/fabrics) type: a compile-time class that executes within the compiler or the IDE. + +```cs +namespace BarnEquipment +{ + internal class Fabric : NamespaceFabric + { + public override void AmendNamespace(INamespaceAmender amender) + { + amender.InternalsCanOnlyBeUsedFrom(from => + from.CurrentNamespace().Or(or => or.Type("**.Tests.**"))); + } + } + + internal class Door; +} + +namespace FieldEquipment +{ + // Warning: BarnEquipment.Door is internal to the `BarnEquipment` namespace. + public class PedenstrianFriendlyGate : BarnEquipment.Door; + +} +``` + +Now, if some foreign code tries to access an internal API of the `BarnEquipment` namespace, a warning will be reported. + +The package includes verification methods like: + +- [InternalsCanOnlyBeUsedFrom](https://doc.metalama.net/api/metalama-extensions-architecture-fabrics-verifierextensions-internalscanonlybeusedfrom) +- [InternalsCannotBeUsedFrom](https://doc.metalama.net/api/metalama-extensions-architecture-fabrics-verifierextensions-internalscannotbeusedfrom) +- [CanOnlyBeUsedFrom](https://doc.metalama.net/api/metalama-extensions-architecture-fabrics-verifierextensions-canonlybeusedfrom) +- [CannotBeUsedFrom](https://doc.metalama.net/api/metalama-extensions-architecture-fabrics-verifierextensions-cannotbeusedfrom) +- [MustRespectNamingConvention](https://doc.metalama.net/api/metalama-extensions-architecture-fabrics-verifierextensions-mustrespectnamingconvention) +- [MustRespectRegexNamingConvention](https://doc.metalama.net/api/metalama-extensions-architecture-fabrics-verifierextensions-mustrespectregexnamingconvention) + +## Verifying Your Own Rules + +If, like _la chèvre de Monsieur Seguin_, you feel confined within the enclosure of predefined methods and yearn for the fresh air of do-it-yourself mountains, we have good news for you. First, you can create your own rules—both custom attributes and programmatic—using Metalama's API. Second, there's no wolf in these mountains. At worst, you might get lost or a bit dazed bu the fresh air, and sheepishly find your way back to the enclosure. + +There are two ways to extend the API: by creating your own _rules_ (like `InternalsCanOnlyBeUsedFrom` or `CannotBeUsedFrom`) or your own _predicates_ (like `CurrentNamespace`). + +For instance, the following snippet defines a `NameEndsWith` predicate that matches members whose names end with a given suffix. + +```csharp +[CompileTime] +public static class Extensions +{ + public static ReferencePredicate NameEndsWith( + this ReferencePredicateBuilder builder, + string suffix ) + => new NameSuffixPredicate( builder, suffix ); +} + +internal class NameSuffixPredicate : ReferenceEndPredicate +{ + private readonly string _suffix; + + public NameSuffixPredicate( ReferencePredicateBuilder builder, string suffix ) : base( builder ) + { + this._suffix = suffix; + } + + protected override ReferenceGranularity GetGranularity() => ReferenceGranularity.Member; + + public override bool IsMatch( ReferenceEnd referenceEnd ) + => referenceEnd.Member is INamedDeclaration method && method.Name.EndsWith( + this._suffix, + StringComparison.Ordinal ); +} + +``` + +This allows you to ensure that your code is only called by _polite_ APIs: + +```csharp +internal class Fabric : ProjectFabric +{ + public override void AmendProject( IProjectAmender amender ) + { + amender.SelectReflectionType( typeof(CofeeMachine) ) + .CanOnlyBeUsedFrom( r => r.NameEndsWith( "Politely" ) ); + } +} +``` + +To explore the rabbit hole, [start here](https://doc.metalama.net/conceptual/architecture/extending). + +## Conclusion + +_Architecturae erosio delenda est._ + +Defining a well-thought-out and consistent architecture is a key phase of any non-trivial software development project. But once the architecture is defined, it shouldn't just end up in a drawer. It must be enforced. + +Unless architecture rules are made executable, they can only be enforced through code reviews, which are costly, slow, and unreliable due to human factors. Code reviews driven by humans will still be important for a long time, but let's automate what can be automated. + +In the previous article, I showed how Metalama can automate your repetitive code writing tasks through on-the-fly code generation. Today, I've demonstrated two ways to express architecture rules using Metalama: with custom attributes and programmatically through fabrics. + +That's the end of my mini-series about Metalama. If you want to know more about Metalama, feel free to [download it](https://www.postsharp.net/metalama/download) from NuGet or the Visual Studio Marketplace. There is a free edition to start with and tons of commented examples and ready-made, open-source implementations on the [Metalama Marketplace](https://www.postsharp.net/metalama/marketplace). The development team is eager to answer your questions on our [Slack workspace](https://www.postsharp.net/slack). + +Happy meta-programming with Metalama! diff --git a/run-all.ps1 b/run-all.ps1 new file mode 100644 index 0000000..0cede33 --- /dev/null +++ b/run-all.ps1 @@ -0,0 +1,2 @@ +# Please DO NOT add the --incremental flag here. You can add it when you _call_ this script instead thanks to `@args`. +bundle exec jekyll build --drafts --unpublished --trace --strict_front_matter @args