CodeBlanks Logo Icon CodeBlanks

Reflection and attributes

C# course · Advanced · Lesson 81/83

Write command handling with reflection and custom attributes.

Reflection and attributes

In the second lesson the command stood alone. Now we mark commands with an attribute and discover them through reflection. The [Command("hello")] attribute gives a class a name; Assembly.GetExecutingAssembly().GetTypes() enumerates all types; t.GetCustomAttribute<CommandAttribute>() reads the attribute.

Result: to add a new command you only write a new class marked with [Command("name")]. Main does not change; reflection finds the command automatically. This is a strong application of the open-closed principle.

The command attribute

The CommandAttribute class derives from Attribute and carries a Name property. Usage: [Command("hello")]. The value in parentheses is passed to the constructor. The attribute adds compile-time metadata to the class; it is read at runtime via reflection.

Note: with AttributeUsage you can restrict the attribute to classes only. For now the default usage is enough; you can add this restriction later.

Exercise Discover commands via reflection

Define a CommandAttribute : Attribute class; its constructor takes a string name and assigns it to a Name property. Mark HelloCommand with [Command("hello")]. In Main, scan all types via Assembly.GetExecutingAssembly().GetTypes(); for each type call t.GetCustomAttribute<CommandAttribute>(). Add types that have the attribute and implement ICommand to a Dictionary<string, Type>. Get the type with commands["hello"], instantiate it with Activator.CreateInstance, cast to (ICommand), and print await cmd.ExecuteAsync().

Show example solution
Attribute { public string Name { get; } public CommandAttribute(string name) => Name = name; }. Mark HelloCommand with [Command("hello")]. In Main: var commands = new Dictionary<string, Type>(); foreach (var t in Assembly.GetExecutingAssembly().GetTypes()) { var attr = t.GetCustomAttribute<CommandAttribute>(); if (attr != null && typeof(ICommand).IsAssignableFrom(t)) commands[attr.Name] = t; } var type = commands["hello"]; var cmd = (ICommand)Activator.CreateInstance(type); Console.WriteLine(await cmd.ExecuteAsync());
Example solution:
using System;
using System.Threading.Tasks;
using System.Reflection;
using System.Collections.Generic;

[Command("hello")]
class HelloCommand : ICommand
{
    public async Task<string> ExecuteAsync()
    {
        await Task.CompletedTask;
        return "Merhaba";
    }
}

class CommandAttribute : Attribute
{
    public string Name { get; }
    public CommandAttribute(string name) => Name = name;
}

interface ICommand
{
    Task<string> ExecuteAsync();
}

class Program
{
    static async Task Main()
    {
        var commands = new Dictionary<string, Type>();
        foreach (var t in Assembly.GetExecutingAssembly().GetTypes())
        {
            var attr = t.GetCustomAttribute<CommandAttribute>();
            if (attr != null && typeof(ICommand).IsAssignableFrom(t))
                commands[attr.Name] = t;
        }

        var type = commands["hello"];
        var cmd = (ICommand)Activator.CreateInstance(type);
        Console.WriteLine(await cmd.ExecuteAsync());
    }
}

Test yourself

  1. Match reflection terms

    [Command("hello")]
    Marks a class with a name
    GetTypes()
    Enumerates all types
    GetCustomAttribute<T>
    Reads the attribute
    Activator.CreateInstance
    Instantiates a type
  2. Complete the attribute usage

    [Command("____")]
    class HelloCommand : ICommand { ... }
    Show answer
    [Command("hello")]
    class HelloCommand : ICommand { ... }
  3. What does reflection-based command discovery add?

    • Faster execution
    • Less code overall
    • Static type safety
    • Adding a new command does not change Main
    Show answer

    Adding a new command does not change Main — Correct. Reflection finds the new command automatically; Main stays the same.

All lessons in Advanced