A modern Roslyn-based
meta-programming framework for C#
to reduce boilerplate and architecture erosion.

What are boilerplate code and
architecture erosion costing you?

A Total Waste of Your Time


Manually writing boilerplate is not only boring and frustrating, it is a total waste of your time. Why do manually what a machine can do better and faster?

Although allowing architecture to erode initially saves time, but as it progressively accumulates, it makes everybody on the team less productive.

Excessive Complexity


Your business logic is unreadable because it is littered with boilerplate and the source code becomes so complex that your brain can no longer make sense of it.

As source code derives from the original architecture, more and more exceptions to the rule are committed, and the conceptual complexity of the source code increases.

Avoidable Defects


Boilerplate is littered with bugs because it's created by copy-paste programming and mostly remains untested. Production-readiness features such as logging or caching are neglected because they are too expensive to build without the proper tooling.

As your codebase complexity increases its overall quality suffers: end-users experience bugs and performance degrades.

Expensive Maintenance


With both boilerplate and architecture erosion increasing complexity, it becomes more and more expensive to make small changes in your application. A bigger refactoring becomes completely impossible, and you need to rewrite your whole codebase from scratch.

Smart use of meta-programming could extend the lifetime of your codebase by several years.

Introducing metalama

Reducing boilerplate and architecture erosion through meta-programming.

Boilerplate Elimination

Offload boilerplate generation to the compiler and keep your source code clean and succinct thanks to aspect-oriented programming.

Free time for meaningful development tasks

Make your application more reliable

Make your codebase easier to read and maintain

Live Templates & Code Fixes

Empower your team with custom code fixes and refactorings tailored to your own architecture, right from the IDE.

Accelerate development

Provide implementation guidance

Make your codebase easier to read and maintain

Architecture Validation

Validate your codebase against your own rules, display design-time squiggles, or fail the build when a violation is found.

Provide immediate feedback to developers.

Keep the codebase consistent with design.

Achieve smoother code reviews.

Aspects:
our core technology

Aspects are special C# classes execute inside the compiler or the IDE and can:

  • enhance your source code at compile-time or in the IDE,
  • validate your source code and emit errors or warnings,
  • suggest code fixes and refactorings.

    How does it
    work?

    • Add a few packages to your project and start hacking meta code.
      Your code remains 100% C#.

    • At design time, Metalama extends standard Roslyn extension points (analyzers, source generators), so it integrates with Visual Studio, Visual Studio Code, and Rider.

    • At compile time Metalama replaces the compiler by our own fork of Roslyn, allowing us to hook into the code generation process.

    • At run time, only a few helper classes are needed. They are released as open source on GitHub.

The result: your code is
shorter and cleaner.

Without Metalama
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
public class Address : INotifyPropertyChanged
{
    private string _line1;
    private string _line2;
    private string _city;
    private string _remark;

    public string Line1
    {
        get => _line1;
        set
        {
            _line1 = value;
            OnPropertyChanged();
            OnPropertyChanged(nameof(FullAddress));
        }
    }

    public string Line2
    {
        get => _line2;
        set
        {
            _line2 = value;
            OnPropertyChanged();
            OnPropertyChanged(nameof(FullAddress));
        }
    }

    public string City
    {
        get => _city;
        set
        {
            _city = value;
            OnPropertyChanged();
            OnPropertyChanged(nameof(FullAddress));
        }
    }

    public string Remark
    {
        get => _remark;
        set
        {
            _remark = value;
            OnPropertyChanged();
        }
    }

    public string FullAddress => string.Join(Environment.NewLine, Line1, Line2, City);

    public event PropertyChangedEventHandler PropertyChanged;

    // TODO: Weak event.
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class Person : INotifyPropertyChanged
{
    private string _firstName;
    private string _lastName;
    private Address _address;

    public string FirstName
    {
        get => _firstName;
        set
        {
            _firstName = value;
            OnPropertyChanged();
        }
    }

    public string LastName
    {
        get => _lastName;
        set
        {
            _lastName = value;
            OnPropertyChanged();
        }
    }

    public Address Address
    {
        get => _address;
        set
        {
            _address = value;
            OnPropertyChanged();
        }

    }


    public event PropertyChangedEventHandler PropertyChanged;

    // TODO: Weak event.
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

}

public class PersonViewModel : INotifyPropertyChanged
{
    private Person _person;
    private bool _includeAddressInDetails;

    // TODO: Weak reference (if weak events are implemented).
    private Address _observedAddress;

    public Person Person
    {
        get => _person;
        set
        {

            if (_person != null)
            {
                _person.PropertyChanged -= OnPersonPropertyChanged;
                if (_observedAddress != null)
                {
                    _observedAddress.PropertyChanged -= OnAddressPropertyChanged;
                    _observedAddress = null;
                }
            }
            _person = value;

            if (value != null)
            {
                value.PropertyChanged += OnPersonPropertyChanged;
                _observedAddress = value.Address;
                if (_observedAddress != null)
                {
                    _observedAddress.PropertyChanged += OnAddressPropertyChanged;
                }
            }
            OnPropertyChanged();
            OnPropertyChanged(nameof(FullDetails));


        }
    }


    private void OnAddressPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(Address.FullAddress))
        {
            OnPropertyChanged(nameof(FullDetails));
        }
    }




    private void OnPersonPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch (e.PropertyName)
        {
            case nameof(Person.FirstName):
            case nameof(Person.LastName):
                OnPropertyChanged(nameof(FullDetails));
                break;

            case nameof(Person.Address):
                if (_observedAddress != null)
                {
                    _observedAddress.PropertyChanged -= OnAddressPropertyChanged;
                    _observedAddress = null;
                }

                _observedAddress = _person.Address;
                if (_observedAddress != null)
                {
                    _observedAddress.PropertyChanged += OnAddressPropertyChanged;
                }

                break;
        }
    }

    public bool IncludeAddressInDetails
    {
        get => _includeAddressInDetails;
        set
        {
            _includeAddressInDetails = value;
            OnPropertyChanged();
            OnPropertyChanged(nameof(FullDetails));
        }
    }

    public string FullDetails
    {
        get
        {
            // We don't handle the nulls for simplicity of the argument.
            if (IncludeAddressInDetails)
            {
                return string.Join(this.Person.FirstName + " " + this.Person.LastName,
                    this.Person.Address.FullAddress);
            }
            else
            {
                return this.Person.FirstName + " " + this.Person.LastName;
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    // TODO: Weak events.
    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
With Metalama
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
[NotifyPropertyChanged]
public class Address
{
    public string Line1 { get; set; }
    public string Line2 { get; set; }
    public string City { get; set; }
    public string Remark { get; set; }
    public string FullAddress => string.Join(Environment.NewLine, Line1, Line2, City);
}

[NotifyPropertyChanged]
public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Address Address { get; set; }
}

[NotifyPropertyChanged]
public class PersonViewModel
{
    public Person Person { get; set; }
    public bool IncludeAddressInDetails { get; set; }

    public string FullDetails
    {
        get
        {
            // We don't handle the nulls for simplicity of the argument.
            if (IncludeAddressInDetails)
            {
                return string.Join(this.Person.FirstName + " " + this.Person.LastName,
                    this.Person.Address.FullAddress);
            }
            else
            {
                return this.Person.FirstName + " " + this.Person.LastName;
            }
        }
    }
}
Without Metalama
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public partial class MainWindow : Window
{
    private MainViewModel _mainViewModel = new MainViewModel();

    public MainWindow()
    {
        InitializeComponent();
        this.OpenFile = new OpenFileCommand(_mainViewModel);
        this.CloseFile = new CloseFileCommand(_mainViewModel);
    }

    public ICommand OpenFile { get; }

    public ICommand CloseFile { get; }

    class OpenFileCommand : ICommand
    {
        private MainViewModel _mainViewModel;

        public OpenFileCommand(MainViewModel mainViewModel)
        {
            _mainViewModel = mainViewModel;
        }

        public bool CanExecute(object parameter) => true;

        public void Execute(object parameter) => _mainViewModel.OpenFile();

        public event EventHandler CanExecuteChanged;
    }

    class CloseFileCommand : ICommand
    {
        private MainViewModel _mainViewModel;

        public CloseFileCommand(MainViewModel mainViewModel)
        {
            _mainViewModel = mainViewModel;

            _mainViewModel.PropertyChanged += delegate (object o, PropertyChangedEventArgs args)
                {
                    if (args.PropertyName == nameof(MainViewModel.IsFileOpen))
                    {
                        this.CanExecuteChanged?.Invoke(this, EventArgs.Empty);
                    }
                };
        }

        public bool CanExecute(object parameter) => _mainViewModel.IsFileOpen;

        public void Execute(object parameter) => _mainViewModel.CloseFile();

        public event EventHandler CanExecuteChanged;
    }
}

public partial class FancyTextBlock : UserControl
{
    public static DependencyProperty TextProperty = DependencyProperty.Register(
      "Text", typeof(string), typeof(FancyTextBlock), null,
      value => !string.IsNullOrEmpty(value as string));

    public string Text
    {
        get => (string)this.GetValue(TextProperty);
        set => this.SetValue(TextProperty, value);
    }
}
With Metalama
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public partial class MainWindow : Window
{
    private MainViewModel _mainViewModel = new MainViewModel();

    public MainWindow()
    {
        InitializeComponent();
    }

    [Command]
    public ICommand OpenFile { get; private set; }

    private void ExecuteOpenFile() => _mainViewModel.OpenFile();


    [Command]
    public ICommand CloseFile { get; private set; }

    private void ExecuteCloseFile() => _mainViewModel.CloseFile();
    public bool CanExecuteCloseFile => _mainViewModel.IsFileOpen;
}

public partial class FancyTextBlock : UserControl
{
    [DependencyProperty, Required]
    public string Text { get; set; }
}
Without Metalama
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class Icecream : IFreezable
{
    private double _temperature;
    public FreezableList<Ingredient> Ingredients { get; } = new FreezableList<Ingredient>();

    public double Temperature
    {
        get => _temperature;
        set
        {
            if (this.IsFrozen) throw new ObjectReadOnlyException();
            _temperature = value;
        }

        // Hello, world.
    }

    public void Freeze()
    {
        this.IsFrozen = true;
        this.Ingredients.Freeze();
    }

    public bool IsFrozen { get; private set; }
}


public class Ingredient : IFreezable
{
    private string _name;
    private double _quantity;

    public string Name
    {
        get => _name;

        set
        {
            if (this.IsFrozen) throw new ObjectReadOnlyException();

            _name = value;
        }
    }

    public double Quantity
    {
        get => _quantity;

        set
        {
            if (this.IsFrozen) throw new ObjectReadOnlyException();

            _quantity = value;
        }
    }

    public void Freeze()
    {
        this.IsFrozen = true;
    }

    public bool IsFrozen { get; private set; }
}
With Metalama
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Freezable]
public class Icecream
{
    public FreezableList<Ingredient> Ingredients { get; } = new FreezableList<Ingredient>();

    public double Temperature { get; set; }
}

[Freezable]
public class Ingredient
{
    public string Name { get; set; }

    public double Quantity { get; set; }
}
Without Metalama
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class UserNameService
{
    private Logger logger;
    private Db db;

    // It's just one method, but you should do this for thousands.
    public string GetUserName(int userId)
    {
        var methodLogger = logger.Start("UserNameService.GetUserName({userId})");

        try
        {

            string userName = db.Context.Users.Single(u => u.Id == userId).Name;
            methodLogger.Success(userName);

            return userName;
        }
        catch (Exception e)
        {
            methodLogger.Exception(e);
            throw;
        }
    }
}
With Metalama
1
2
3
4
5
6
7
8
9
10
11
public class UserNameService
{
    private Db db;

    // Logging is generally configured in an xml config file.
    public string GetUserName(int userId)
    {
        return db.Context.Users.Single(u => u.Id == userId).Name;
    }
}
Without Metalama
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public interface IBananaFactory
{
    Banana GetBanana(string kind, double length, double diameter);
}

public class BananaFactory : IBananaFactory
{
    public Banana GetBanana(string kind, double length, double diameter)
    {
        if (string.IsNullOrEmpty(kind)) throw new ArgumentNullException(nameof(kind));
        if (length < 0) throw new ArgumentOutOfRangeException(nameof(length));
        // Yep copy-pase error again.
        if (length < 0 || diameter > 5) throw new ArgumentOutOfRangeException(nameof(length));

        return new Banana(kind, length, diameter);
    }
}

public class Banana
{
    private string _kind;
    private double _length;
    private double _diameter;

    public Banana(string kind, double length, double diameter)
    {
        Kind = kind;
        Length = length;
        Diameter = diameter;
    }

    public string Kind
    {
        get => _kind;
        set
        {
            if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value));
            _kind = value;
        }
    }

    public double Length
    {
        get => _length;
        set
        {
            if (value < 0) throw new ArgumentOutOfRangeException(nameof(value));
            _length = value;
        }
    }

    public double Diameter
    {
        get => _diameter;
        set
        {
            if (value < 0 || value > 5) throw new ArgumentOutOfRangeException(nameof(value));
            _diameter = value;
        }
    }
}
With Metalama
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public interface IBananaFactory
{
    Banana GetBanana([Required] string kind, [Positive] double length, [Range(0, 5)] double diameter);
}

public class BananaFactory : IBananaFactory
{
    public Banana GetBanana(string kind, double length, double diameter)
    {
        return new Banana(kind, length, diameter);
    }
}

public class Banana
{
    public Banana(string kind, double length, double diameter)
    {
        Kind = kind;
        Length = length;
        Diameter = diameter;
    }

    [Required]
    public string Kind { get; set; }

    [Positive]
    public double Length { get; set; }

    [Range(0, 5)]
    public double Diameter { get; set; }

}
n class="codePunctuation">}
INPC
XAML
THREADING
LOGGING
CONTRACTS

What is holding you back?

I can do the same with open-source tools.

You may think of with Roslyn analyzers, Roslyn code generators, or open-source IL tools.

But Metalama plays in a different league.

Metalama is integrated. No other tool than Metalama allows you to both generate code that is visible at design time, and override hand-written code. You would need to use a combination of open-source tools, which do not integrate with each other, in order to achieve can Metalama can do.

Metalama is much simpler. Doing meta-programming well at at the abstraction of Roslyn or MSIL is very complex (think years of experience needed, not months). Done naively, low-level metaprogramming is comparable to hacking. It can significantly increase complexity, especially after the developer who implemented the meta-code leaves the team.

Metalama is engineered for good architecture. Metalama is designed to simplify development. It does not offer hacks that make the code less predictable or understandable. There are three cultures in software development: hacking, science, and engineering. We belong to the last one, while staying attentive to both other.

Metalama is a complete, well-though-out solutions. Alternatives focus on the most frequent use cases and cut corners – for instance they don’t not properly implement async methods. PostSharp goes the extra mile and adds enough extension points so that you never get stuck.


I will lose control over code.

Delegating is what you actually want. You want to have the machine take care of low-level repetitive details, so you have more time to focus on more meaningful tasks.

You are already used to that. People were already afraid of losing control over performance when the industry moved from assembly language to C, and then when moving to the managed memory, garbage-collected model of C# and Java. We’ve lost control over low-level details but got huge productivity and reliability gains.

You are not completely losing control. PostSharp gives you a lot of mechanisms to override the default behaviors.

You can come back to source code at any time. There is no vendor lock in.


I will no longer understand what the code is doing.

We believe you will understand your code much better than before.

Debug the transformed code. It is possible that you will notice a behavior that you cannot understand solely by reading the source code. You can, at any time, configure Metalama to step into the transformed code instead of the source code, and you will see exactly what is happening.

Use the diff preview. You can compare the source code with the transformed code from Visual Studio even without rebuilding the solution.


I am already using Resharper or Rider.

You can use both. We are also fans of Rider.

Metalama reduces the amount of code. Refactoring tools make developers write code faster, but do not reduce the amount of code necessary to implement a feature.

Metalama makes it easy to write custom rules and code fixes. Other refactoring tools have much more complex APIs.


How does it compare?

Metalama Roslyn Fody PostSharp
Technology Roslyn Roslyn MSIL MSIL
Transforming the compilation using low-level APIs
Yes No Yes Yes
Adding behaviors to source code simply using aspects.
Yes No Very limited Yes
Introduce new members or interfaces and reference them in source code.
Yes Difficult No No
Analyze source code and report warnings and errors.
Yes Difficult Requires rebuild. Requires rebuild.
Debug and export transformed code.
Yes N/A No No

Success stories

Siemens Audiology
ATS Global
Thales
Gamesys
CognitiveX
siemens-case-study

Siemens Audiology

Siemens Audiology saved 15% development time plus improved code readability by removing unwanted boilerplate code.

When the team at Siemens Audiology was tasked with building a new WPF implementation for two of its leading hearing system software applications, a big challenge was to find a way to save coding time for developers implementing ViewModels while increasing code readability.

ats-global-casestudy

ATS Global

ATS Global saves 16% lines of code.

When the team at ATS Global needed to build a complex shop floor simulation, they faced the challenge of potential multithreading issues and the complexity of writing synchronization code.

The team turned to PostSharp and was able to write thread-safe code without training all team members in advanced multithreading and delivered required features with 16% fewer lines of code.

thales-casestudy

Thales Information Systems

Thales is more focused on business logic.

When the team at Thales Information Systems was tasked with a big refactoring on a project, one of the challenges was to simplify architecture by reducing boilerplate code. The architect chose PostSharp to implement custom aspects to handle logging, performance counters and INotifyPropertyChanged and keep junior team members better focused on business logic.

gamesys-casestudy

Gamesys

Gamesys improves productivity with PostSharp.

Gamesys serves around one million daily active users across its social games. Their backend services handle more than 250 million requests per day. Despite its massive scale, this distinctive service is being maintained by a remarkably small development team of just seven super-productive individuals.

cognitivex-casestudy

Cognitive X Solutions

PostSharp took care of 95% of CognitiveX's INPC code.

When Cognitive X was looking for ways to deliver value to their clients while differentiating themselves from the competition, the team chose PostSharp as one of the core pieces of their strategy. By decreasing the actual lines of code they were writing, as well as helping them enforce best practices, PostSharp allowed them to produce new features for their clients faster, at lower cost, and with fewer errors.

Get started with Metalama.

More than 10% of all Fortune 500 companies have already chosen PostSharp.