Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Metaprogramming in C#

You're reading from   Metaprogramming in C# Automate your .NET development and simplify overcomplicated code

Arrow left icon
Product type Paperback
Published in Jun 2023
Publisher Packt
ISBN-13 9781837635429
Length 352 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Einar Ingerbrigsten Einar Ingerbrigsten
Author Profile Icon Einar Ingerbrigsten
Einar Ingerbrigsten
Arrow right icon
View More author details
Toc

Table of Contents (25) Chapters Close

Preface 1. Part 1:Why Metaprogramming?
2. Chapter 1: How Can Metaprogramming Benefit You? FREE CHAPTER 3. Chapter 2: Metaprogramming Concepts 4. Chapter 3: Demystifying through Existing Real-World Examples 5. Part 2:Leveraging the Runtime
6. Chapter 4: Reasoning about Types Using Reflection 7. Chapter 5: Leveraging Attributes 8. Chapter 6: Dynamic Proxy Generation 9. Chapter 7: Reasoning about Expressions 10. Chapter 8: Building and Executing Expressions 11. Chapter 9: Taking Advantage of the Dynamic Language Runtime 12. Part 3:Increasing Productivity, Consistency, and Quality
13. Chapter 10: Convention over Configuration 14. Chapter 11: Applying the Open-Closed Principle 15. Chapter 12: Go Beyond Inheritance 16. Chapter 13: Applying Cross-Cutting Concerns 17. Chapter 14: Aspect-Oriented Programming 18. Part 4:Compiler Magic Using Roslyn
19. Chapter 15: Roslyn Compiler Extensions 20. Chapter 16: Generating Code 21. Chapter 17: Static Code Analysis 22. Chapter 18: Caveats and Final Words 23. Index 24. Other Books You May Enjoy

Removing manual structure and process

Adding explicit metadata is great for visibility and makes it very explicit in the code for the type of metadata that has been added. However, this metadata is not actionable on its own. This means that there is nothing that will inherently deal with it – for instance, a property is required, as we’ve seen.

This metadata gives us the power to not only reason about the metadata surrounding our code but put it into action. We can build our systems in a way that leverages this information to make decisions for us, or we could automate tedious tasks.

One of the most common things I’ve seen throughout my career is what I call recipe-driven development. Code bases tend to settle on a certain structure and a certain set of things developers need to do when creating features in it. These recipes are then often written down as a part of the documentation for the code base and something everyone has to read and make sure they follow. This is not necessarily a bad thing, and I think all code bases have this to some degree.

Taking a step back, there might be some potential to improve our productivity and have to write less code. The recipes and patterns could be formalized and automated. The main reason for doing so is that following recipes can be error-prone. We can forget to do something or do it wrong or maybe even mix up the ordering of steps.

Imagine that you have an API and that for every action, you have the following recipe:

  • Check if the user is authorized
  • Check if all the input is valid
  • Check for malicious input (for example, SQL injection)
  • Check if the action is allowed by the domain logic, typically business-specific rules
  • Perform the business logic
  • Return the correct HTTP status code and result, depending on whether or not we’re successful

Those are a lot of concerns mixed into one place. At this point, you’re probably thinking that this is not how we do things in modern ASP.NET API development. And that is correct – they are typically split into concerns and things such as the SQL injection handled by the pipeline.

Important note

We’ll revisit how ASP.NET leverages metaprogramming to make the developer experience it offers in Chapter 3, Demystifying through Existing Real-World Examples.

Even though these things might not be in the same method and spread out, they are still concerns we have to be aware of, and a recipe would then still state that these things would need to be done. Often, they are repetitive and could potentially be optimized for an improved developer experience and also reduce the risk of fatal errors in the system.

Maintaining software

Another aspect of this type of repetitive code is that all code we add to our system is code we need to maintain. Building out a feature might not take that long, but chances are the code needs to be maintained for years to come. It might not be maintained by you, but by others on the team or a successor to you. So, we should be optimizing our code bases for maintenance first. Getting features out the door in a timely fashion is expected of us, but if we don’t consider the maintenance of the code, we, as the owners of the code, will suffer when it needs to be maintained.

Maintenance is not just about keeping the code working and delivering on its promise. It’s also about its ability to change and adapt to new requirements, whether business or technical. The very beginning of a project is when you know the least about it.

So, planning for this is super hard and would require us to be able to predict the future. But we can write our code in a way that would make it more adaptable to change.

Instead of repeating all this code all over the place, we could put metadata into our code that we could leverage. This is typically what ASP.NET supports – for instance, for authorization with the [Authorize] attribute for controllers. It would require a specific policy to be fulfilled, such as the user having to be in a role. If our system has a deliberate structure for our features, you might find natural groupings of features belonging to specific roles. We could then reason about this structure by looking at the namespace metadata on the type and putting in the correct authorization rules. For developers, you replaced the need for an explicit piece of information and made it implicit through the structure. This may seem like a small thing, but throughout the lifetime of the code base, this type of mindset can have a huge impact on productivity and maintainability.

Generating code

With C#, we can go even further than just reasoning about code and making decisions based on what we find – we can generate code. Code generation can take place at compile time if we have all the information we need or are pushed to the runtime level. This opens up a lot more flexibility and gives us a vast amount of power.

As an example, if you’ve ever worked with XAML-based frontend technology such as Windows Presentation Foundation (WPF) or Universal Windows Platform (UWP) and have used data binding, you have probably come across the INotifyPropertyChanged interface. Its purpose is to enable the view controls so that you’re notified when the value of a property has changed on an instance of an object in the view to which it is bound.

Let’s say you have an object representing a person:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Now, let’s say we want to make this notification appear whenever one of the properties changes. Using the INotifyPropertyChanged interface for binding purposes, the object would need to expand into the following:

public class Person : INotifyPropertyChanged
{
    private string _firstName;
    public string FirstName
    {
        get { return _firstName; }
        set
        {
            _name = value;
            RaisePropertyChanged("FirstName");
        }
    }
    public string LastName { get; set; }
    public event PropertyChangedEventHandler
      PropertyChanged;
    protected void RaisePropertyChanged(string
      propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new
              PropertyChangedEventArgs(propertyName));
        }
    }
}

As you can see, creating a property is now very tedious. Imagine having all the properties of an object do this. This easily becomes code that is hard to read, and there's more code to maintain, and it’s not adding any business value to your code.

This can be improved upon thanks to the latest version of the C# compiler.

Microsoft rewrote the C# compiler a few years back. The compiler was given the name Roslyn. There were a couple of reasons they rewrote the compiler, with one being that they wanted to have the compiler itself be written in C# – a proof of the maturity of the language and the .NET runtime Also, as part of the move from Microsoft to open source, having a rewrite and doing it in the open and leaving the old license model behind made more sense. But the most important reason in my opinion was to make it more extensible, and not just for Microsoft themselves, but everyone.

Part of this extensibility is what is called Roslyn code generation. With it, we could go and make this code very close to the original. Let’s imagine we introduce some metadata in the form of an [Bindable] attribute and we create a compiler extension that makes all private fields into properties that are needed for InotifyPropertyChanged. Here, our object would look like this:

[Bindable]
public class Person
{
    private string _firstName;
    private string _lastName;
}

We could also do this at runtime. However, at runtime, we are limited to what has been compiled and can’t change the type. So, the approach would be slightly different. Instead of changing the existing type, we would need to create a new type that inherits from the original type and then extend it. This would require us to make the original properties virtual for us to override them in a generated type:

[Bindable]
public class Person
{
    public virtual string FirstName { get; set; }
    public virtual string LastName { get; set; }
}

For this to work, we would need a factory that knows how to create these objects. We would call this when we needed an instance of it.

With great power also comes great responsibility, and it needs to be a very deliberate choice to go down this path. We’ll cover this in Chapter 18, Caveats and Final Words.

We will cover the Roslyn extensibility in more depth in Chapter 15, Roslyn Compiler Extensions.

Compile time safety

There are also times when we must add certain metadata for the system to work. This is a candidate for writing a code analyzer for the Roslyn compiler. The analyzer would figure out what’s missing and let the developer know as soon as possible, providing a tight feedback loop rather than the developer having to discover the problem at runtime.

An example of this is in a platform I work on called Cratis (https://cratis.io), an event sourcing platform. For all the events being persisted, we require a unique identifier that represents the type of event. This is added as an attribute for the event:

[EventType("66f58b90-c027-41b3-aa2c-2cfd18e7db69")]
public record PersonRegistered(string FirstName, string LastName);

When calling the Append() method on the event log, the type has to be associated with the unique identifier. If there is no association between an event type and the .NET type, the Append() method will throw an exception. This is a great opportunity to perform a compile-time check of anything being sent to the Append() method and to check whether or not the type of the object has the [EventType] attribute.

We will revisit all this in Chapter 17, Static Code Analysis.

You have been reading a chapter from
Metaprogramming in C#
Published in: Jun 2023
Publisher: Packt
ISBN-13: 9781837635429
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime