Hyston blog
About • RSS

Using C# Reflection and Expressions for refactoring

2020-06-19 11:39

disclaimer: here is example of refectoring, that is not optimised and well polished. With more experience I'll find better way to achieve what is written here. I have heavy feeling, this is the example of code, that I'll look back N years later with only one thought: "who, the ..., wrote this??"
However, I want to share this experience anyway to have a reference later.

Just imagine situation: you have 40+ classes with same interface and all of them have function with same signature and almost same body except one generic parameter, that is class type itself.

interface IBaseJob
{
    void Run(IJobCancellationToken token);
}

public class ConcreteJob : IBaseJob
{
    public void Run(IJobCancellationToken token)
    {
        /* implementation */
    }

    public Task Execute() =>
        BackgroundJob.Enqueue<ConcreteJob>(job => job.Run(JobCancellationToken.Null));
}

I usually remove unnecesarry classes and types from examples, but these classes from Hangfire library would be important later.

Here I dont like Execute function, that is repeated in several dosen other classes with only small difference:

public Task Execute() => BackgroundJob.Enqueue<AnotherConcreteJob>(job => job.Run(JobCancellationToken.Null));

That sounds like a opportunity to move some functionality into base class. In C#8 it is possible to add interface default implementation, but we still use C# 7.3, so all I could do is to replace interface with abstract class.

public abstract class IBaseJob
{
    public abstract void Run(IJobCancellationToken token);

    public Task Execute(IJobExecutionContext context)
    {
        //@todo: call here BackgroundJob.Enqueue<ThisJobType>(job => job.Run(JobCancellationToken.Null));
    }
}

...and all duplicated Execute functions from all jobs can be thrown away! 🥳 However, we still have a small problem here: How to call Enqueue method properly? We cannot call it directly, because we are not aware of class type in compile time, so we need to construct this function.

In computer science, reflection is the ability of a process to examine, introspect, and modify its own structure and behavior. Not to be confused with Self-Reflection (Philosophy).
Wikipedia

Well-experienced C# developer have access to types, methods and all stuff, that was used to write code, that he is currently writing 🤪 Start with type Type, that describes type. Enqueue is a static function from BackgroundJob, therefore we need this type and get list of methods, that it supports:

typeof(BackgroundJob)   // return Type in runtime
    .GetMethods()       // return list of available methods

At first I thought just to get method by name (.GetMethod("Enqueue")), but there are 4 methods with same name and friendly exception kindly asked me to be less ambiguously. This is a common problem with reflection code: you usually doesnt know if something is wrong in compile time, all errors show itself only when you actually run the code.
So, method, that I needed, is 1) generic and 2) have Expression<Action<T>> as argument. What is an Expression and why they need Action I will explain later.
GetMethod return MethodInfo, class, that describes all information about concrete method. Then we need to call GetParameters to, well, get method parameters. I'll probably better explain it by adding comments into code:

var enqueueMethod = typeof(BackgroundJob)
    .GetMethods() // We have all BackgroundJob methods here
    .Where(m =>
        m.Name == "Enqueue" &&
        m.IsGenericMethod &&
        m.GetParameters().Length == 1) // Filter out only generic, with proper name and one parameter
    .Select(m => new
    {
        method = m,
        paramType = m.GetParameters().Single().ParameterType
    })  // We need to have a paramter type for further investigation and same MethodInfo in tuple to return it later
    .Where(p =>
        p.paramType.GetGenericTypeDefinition() == typeof(Expression<>) &&   // Generic type of argument should be 'Expression<>'
        p.paramType.GetGenericArguments().Length == 1 &&
        p.paramType.GetGenericArguments().First().GetGenericTypeDefinition() == typeof(Action<>)) // We need to go deeper and find generic arguments from generic arguments 🧐
    .Select(p => p.method) // If single parameter has type Expression<Action<>>, we are good
    .Single()   // An I'm sure, that there is only one of them. However, that's a bad practice, if someone update Hangfire version and there would be another method, that will pass by this criteria

And now we have MethodInfo about what we need to call!
Can we can call it? No, not yet.
Remember that 'Action<T>' ? We need to place current job type there. Type can be found easily with GetType(). Difference with typeof is that later is used in runtime, unlike that GetType() user can place current type in compile type, that is what we needed. And then we construct proper method using MakeGenericMethod

var concreteJobType = GetType();
enqueueMethod = enqueueMethod.MakeGenericMethod(concreteJobType);

Now we are talking!
But we still have one small detail - Enqueue method is not just genetic, it take generic argument. And this argument is itself a Expression that take Action as a generic argument, which broke my brain for a second, when I was trying to figure it out.🤯

An expression is a sequence of one or more operands and zero or more operators that can be evaluated to a single value, object, method, or namespace.
Microsoft docs

This haven't done anything clearer for me. Expression is a smallest part of code, that can be described: variable, constant, function or methods argument. Let's say we have code line a + 3. We can represent it as expression, where a would be Expression.Variable, then 3 would be Expression.Constant and whole line would be Expression.Add with previous two as a parameters. Lambda itself can be presented as a Expression too1. And now we need to build expression, that represent code job => job.Run(JobCancellationToken.Null) where job would be current type.

var cancellationTokenArgument = Expression.Constant(JobCancellationToken.Null, typeof(IJobCancellationToken));
var jobParameter = Expression.Parameter(concreteJobType, "job");
var runMethodInfo = concreteJobType.GetMethod("Run");
var callExpression = Expression.Call(jobParameter, runMethodInfo, cancellationTokenArgument);
var lambda = Expression.Lambda(callExpression, jobParameter);

So, finaly, we got everything: method, that we need to call and parameters to call it. We can do magic with MethodInfo.Invoke:

object[] args = { lambda };
enqueueMethod.Invoke(null, args);

Couple notes to this code:

Whas it all worth it?

So, now we have one function in base class instead of many (many many) small functions in dosens of files. Does code became clearer? I doubt so. Does it faster? Definitly not 2. We just followed DRY principle and removed duplicated code. I guess, most valulable benefit, that we gain is the experience in writing code, that works with Reflections and Expressions. In writing day-to-day buiseness logic I not often have a chance to use them 😋
We have another example for using reflections in our project: every time someone add new field to class, that represents possible user rights, startup tool make changes to database to add new fields in users table. Otherwise, Reflections and Expressions are used in more common system code like Linq, Automapper etc.


  1. I'm also new to all this terminology. As I understood, 'lambda' mean usually 'lambda expression', but Action and Func types are called delegate types, which is already compiled code.

  2. Expressions should be compiled to IL to run and its a costly process. Not sure, where exactly it is happening here, I guess, already on Hangfire side. Need to run time test for this particular case, but in general, using expression trees can cause performance loss.


Quarantine purchases

2020-06-03 12:06

⌨️ Because of unusual circumstances, regular day I spend on windows laptop before lunch and on macbook after. Both of them in clamshell mode, with closing lids and connected to same monitor, that I brought from work.
For mac I used magic keyboard for last several years, but my hands refuse to type on apple keyboard in windows. Even after I remapped in win10 using SharpKey ⌘ key to ctrl, and ⌥ to alt, it feels clumsy and inappropriate. So I started looking for new keyboard. My requirements were simple:

I haven't found many options. Actually, Keychron K1 was the only one that met every criteria. I'm sure, that there are more, but thats the one that I get and I'm happy about it. And I kinda get all this buzz about mechanical keyboards, it is really nice feel to type on, even on such small profile. I even thought about connecting it to the mac, but for me this is already a windows keyboard. It has switch mac/win, but I just cant press "⌥ + Q" on keyboard, where are used to press "alt + F4" and vice versa.
There are some minor throwbacks, too. Rgb lights are uneven and gimmicky (by my opinion). Keyboard has a dosen types of keys light, but I have found only two of them useful and not distracting. Sometimes key switches (I used red) are too sensible and key started being detected even if I just lay hand on keyboard.
keychron
🎧
My other purchase was motivated by my son. After he dropped my beloved Bose QC25, their right ear speaker stopped working. I always said, that this was the best purchase in my professional career – these headphones helped me survive in noisy offices for several years. Even I'm not in office anymore, I simply cannot concentrate on code without closed headphones on my head, ideally, with noise cancellation. After a week without headphones at home, I got sick from kids cries with TV in next room and ordered Bose 700 (they have full name, but I don't care). And this is the best thing, that I have done to improve my productivity during quarantine. When I wear them, I block myself from other world and concentrate on my tasks. I'm not impressed by sound quality (even QC25, AirPods and cheap JBL plugs sound better, by my opinion), but bose 700 are the most comfortable and block completely surrounding sounds. When I turn them off, I have a feeling, that I'm on plane, that is taking off - suddenly I hear roaring ventilators from both laptops. And it is possible to connect these headphones to two devices, usually for me they are connected to iPhone and XPS15. There are one issue, related to that: if one device start playing sound (doesn't matter, which one), the another device became muted (without pausing playback). As example, when I listen to video or music on laptop and quickly check phone, then sound of locking phone mute sound on laptop until I'll click stop/play again. Minor issue, but annoying. With pair iPhone/mac volume is also muted, but audio switches back very quickly. bose700

Last purchase, that made home life considerably nicer - JBL Flip 5. Now I can listen relaxing jazz with my family or some cheerful hip-hop/rock while cooking. 🎸


Blog update

2020-05-22 06:05

I finally added info about myself and rss feed!


Visual Studio Codespaces

2020-05-21 11:05

MsBuild 2020 is happening right now and I'm trying to catch and learn many interesting stuff from there. By now, the most exciting part was Visual Studio Codespaces, previously known as VS Online. I have already tried it about half a year ago and, as I remember, then it allowed only to create host machine in the cloud. But now it is possible to create free self-hosted environment and, basically, connect from anywhere to development machine. Well, there are some asterisks.
As usually, there are some good guidance on docs.microsoft, here is a good place to start. For cloud environment process is relatively trivial: create microsoft account, create azure account (at this step you need to add credit card credentials), open Visual Studio Online and create host. Of course, it take a lot of time between page redirection and entering account credentials. In Safari this page stuck in endless authorization loop, firefox also refused it to open, use Chrome. Then in desktop VS Code install "Remote Explorer" extension and (also after endless login in microsoft, azure and github account) it is possible to connect into cloud machine and use it as local. Well, almost. Here is list of things, that I have tried, but didn't work well:

visual studio codespace on ipad

BUT This is actual, real and powerful IDE, running in browser! And I didn't use Edge browser1, hopefully Safari support will come soon. On one of questions in VS codespaces Q&A videos, host called iPad support as one of their nearest priorities. That would be awesome🤩


  1. it will take a while until I will start trust to MS browser. I'm old enough to remember supporting IE6.


ELK

2020-05-13 10:20

I was working on adding logs into elastic search from one of our micro API projects. I learned a little bit, that I'll probably forget when I'll deal with this next time. So, I'll write it here.

  1. ELK - Elastic Stack. How it works:
  1. It's very useful to run ELK Stack local. Of course, installing everything will take too much time (I still remember painy days spent installing lamp). Of course, these days everything is in container and there is one good repository, that will help running everything in minutes. All you need to do is:
git clone https://github.com/deviantony/docker-elk.git cd .\docker-elk\ docker-compose up

And read ReadMe.md for further details.

  1. In my case, we have configured sentry and adding additional logging system, without removing old one. In this case better to remove "Logging" section in appsettings, otherwise it will affect both system. Most tutorials recommend start logging in Program.cs before actual werver will start, but I find it too cumbersome. If server didn't started, we will figure it out by not seing swagger page or by errors from other service, that consume this api.
  2. Also, most tutopials did not mention, that serilog can be configured not in code, but in appsettings. Probably, additional libraries needs to be installed, but then startup code itself is not polutted with config lines.
  3. When kibana resist to show logs, it means that, probably, indecies were not setted up correctly. And keep in mind, that it takes some time for log to appear, even if everything is running locally.
  4. Some usefull links:

This is probably too much info for such easy topic, but I might find it usefull some day later and - hey - I'm blogging again!😊


← Next Previous →