In my previous article was presented a way how you can improve reflection performance in some scenarios. Now I want to show how you can make it safer. The source of inspiration will be TypeScript. The keyof
keyword to be precise.
What keyof
does in TypeScript
The keyof
keyword allows you to declare a special enum-like type containing only the associated type's property names. Let's have a look at the folloving example:
interface User {
firstName: string,
lastName: string
}
function dotIt<T>(value: T, propertyName: keyof T) : any {
return value[propertyName]
}
let user: User = { firstName: 'Jon', lastName: 'Smith' }
let firstName = dotIt(user, 'firstName') // no error
let lastName = dotIt(user, 'lastName') // no error
let missingProperty = dotIt(user, 'test') // error because there is not property 'test'
It is a convenient way to make sure that nothing breaks when trying to get property value by property name.
The keyof
in C#
Now let's build an identical sample for C# utilizing the AOT.Reflection library from the previous article. It looks like that;
using Apparatus.AOT.Reflection;
var user = new User { FirstName = "Jon", LastName = "Smith" };
var firstName = DoIt(user, "FirstName");
var lastName = DoIt(user, "LastName");
var missingProperty = DoIt(user, "Test");
object DoIt<T>(T value, string propertyName)
{
var property = value.GetProperties()[propertyName];
if (property.TryGetValue(value, out var propertyValue))
{
return propertyValue;
}
return null;
}
class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
As you may guess, everything will compile and we will get an exception at runtime. If we investigate further, we will find out that C# doesn't have any way to express the idea like that. It wasn't new for me. First of all the TypeScript is the script language that works on top of JavaScript. The keyof
looks wonderful feature in this context. At the same time, the C# is the strongly typed language where you can't just take a value by property name so easily. But if you take to account libraries like the AOT.Reflection then you can start wondering 'what if?' and 'why not?'
Meet KeyOf<T>
After a brief investigation, I found that adding such a concept to C# will be relatively easy. So, I started working on it because it would be a nice addition for AOT.Reflection in multiple ways.
Firstly, it makes your code safer because it allows you to validate it during the compilation stage.
Secondly, if you read the article about AOT.Reflection, you know that it has some limitations while interacting with the generics. The KeyOf<T>
allows us to overcome some of them. For example, we have no idea what type to use in the generic method because it is just a T
. Also, there is no indication in the method signature that can help figure out a reflection inside. With KeyOf<T>
we have such indication. Let's have a look at how to use it by rewriting the previous sample.
using Apparatus.AOT.Reflection;
var user = new User { FirstName = "Jon", LastName = "Smith" };
var firstName = DoIt(user, "FirstName"); // no error
var lastName = DoIt(user, "LastName"); // no error
var missingProperty = DoIt(user, "Test"); // compilation error
object DoIt<T>(T value, KeyOf<T> propertyName)
{
var property = value.GetProperties()[propertyName];
if (property.TryGetValue(value, out var propertyValue))
{
return propertyValue;
}
return null;
}
class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
The only part that changed is the signature of the method. Now it expects KeyOf<T>
as a property name. This change allows us to add indications about the intention to use the reflection inside. So, we can run the source generator on it, and, at the same, it is the obvious place to analyze with the Roslyn analyzer. As a result, we can find all invocations and report a compilation error if you pass the wrong name.
Right now, the analyzer checks of the next case:
- string as keyof:
DoIt(user, "FirstName"); // no error
DoIt(user, "Test"); // compilation error
- nameof as keyof:
DoIt(user, nameof(User.FirstName)); // no error
DoIt(user, nameof(User)); // compilation error
- variable as keyof:
var propertyName = "FirstName";
DoIt(user, propertyName); // compilation error, can't be validated at compile time
if(KeyOf<User>.TryParse(propertyName, out var key))
{
DoIt(user, key); // no error
}
- const as keyof:
const string propertyName = "FirstName";
DoIt(user, propertyName); // no error, can be validated at compile time
if(KeyOf.TryParse(propertyName, out var key))
{
DoIt(user, key); // no error
}
It is possible that I forgot about something. So, if you know how to break it, please create a ticket!
How to use it in your project
To make it work, just install the NuGet package Apparatus.AOT.Reflection
:
dotnet add package Apparatus.AOT.Reflection
Then you will have access to aot reflection and KeyOf<T>
itself. The reflection is not the only place where you can use it. For example, it works nicely when you need to generate some sql for tools like Dapper
, when you need to serialize something, managing bindings in WPF, Xamarin, and AvaloniaUI.
Conclusion
When I started working on the KeyOf<T>
I was fascinated by the abilities that Roslyn analyzers and source generators can give you and how easily you can add new concepts with them. Stay tuned. The name Improving C# with TypeScript
was selected for a reason. More TypeScript features are coming to C# soon.
Top comments (2)
does it have intellisense like typescript?
No. At the moment, there is no way to add custom IntelliSense support via the Nuget package.