Reflections on Using C# Reflection on F# Code
.NET, the framework on which C# runs, supports multiple languages, one of which is F#, a functional programming language. We developed Sekiban, an event sourcing CQRS framework, using C#.
I have been trying various things for a while, thinking that F# running on the .NET Framework could also be used.
I tried to use AsyncEnumerable and static interface method with F# before, but I could not complete it due to my lack of knowledge at that time. However, this time, I added a feature that allows you to write commands without using Enumerable or AsyncEnumerable on the C# Sekiban side. I did this to simplify the settings and make it possible to execute and write functionally in C#, but it was not intended to be used with F#, but as a result, I thought it might be possible to write in F#, so I tried again to write domain code (Aggregate, Commands and Events) in F#.
Domain code written in F#
Here is the resulting code. The code below is a domain command for creating a client.
Verify that the specified BranchId exists
Verify that an account with the specified email has not been created
Register the ClientCreated event
The code for this is as follows:
type CreateClient =
{
[<Required>]Name: string
[<Required>]Email: string
[<Required>]BranchId: Guid }
interface ICommandWithHandlerAsync<Client, CreateClient> with
member this.GetAggregateId() = Guid.NewGuid()
static member HandleCommandAsync(command, context) =
context
.ExecuteQueryAsync(BranchExistsQuery(command.BranchId))
.Verify(fun exists ->
if exists then
ExceptionOrNone.None
else
ExceptionOrNone.FromException(InvalidDataException("Branch not exists")))
.Conveyor(fun () -> context.ExecuteQueryAsync(ClientEmailExistsNextQuery(command.Email)))
.Verify(fun exists ->
if exists then
ExceptionOrNone.FromException(InvalidDataException("Email not exists"))
else
ExceptionOrNone.None)
.Conveyor(fun () ->
context.AppendEvent(ClientCreated(command.Name, command.Email, command.BranchId)))
Here is the C# code that performs the same operation:
public record CreateClientR(
[property: Required]
Guid BranchId,
[property: Required]
string ClientName,
[property: Required]
string ClientEmail) : ICommandWithHandlerAsync<Client, CreateClientR>
{
public Guid GetAggregateId() => Guid.NewGuid();
public static Task<ResultBox<UnitValue>>
HandleCommandAsync(CreateClientR command, ICommandContext<Client> context) => context
.ExecuteQueryAsync(new BranchExistsQueryN(command.BranchId))
.Verify(exists => exists ? ExceptionOrNone.None : new InvalidDataException("Branch not exists"))
.Conveyor(_ => context.ExecuteQueryAsync(new ClientEmailExistQueryNext(command.ClientEmail)))
.Verify(exists => exists ? new InvalidDataException("Email not exists") : ExceptionOrNone.None)
.Conveyor(
_ => context.AppendEvent(new ClientCreated(command.BranchId, command.ClientName, command.ClientEmail)));
}
Originally, I was using a library created for writing in C#, and I think you can see that the C# and F# code are almost the same. In fact, I am currently studying how to write in a more F#-like way.
The domain code that includes other code can be found here.
The cause of the error on the C# side when executing the F# domain code
I tried to run the above code by loading the C# library, but encountered a problem where it didn't work properly. The issue occurred when passing an F# class to the code that obtains the method using reflection. The definition on the F# side looks like this:
type Branch =
{ Name: string }
interface IAggregatePayload<Branch> with
static member CreateInitialPayload(_: Branch) : Branch = { Name = "" }
I executed the following code on the C# side that uses reflection to obtain the method information for CreateInitialPayload
.
var method = typeof(TAggregatePayload).GetMethod(
nameof(IAggregatePayloadGeneratable<SnapshotManager>.CreateInitialPayload),
BindingFlags.Static | BindingFlags.Public);
When you execute this against a class written in C#, it finds the method named "CreateInitialPayload" and you can execute it. However, when you execute this against the above F# class, the method returns null and cannot be found.
This issue occurs because, when you look at the Type class of F#'s Branch, the method name is registered with the following string:Sekiban.Core.Aggregate.IAggregatePayloadGeneratable<fsCustomer.Domain.Branch>.CreateInitialPayload
In other words, the method name is registered as InterfaceName.MethodName
.
Since Sekiban uses reflection in various places, this caused a bit of a dilemma.
Solving the GetMethod Issue with Reflection on F# Classes
After considering the solution, I felt that since we can retrieve all methods with GetMethods(), we should be able to find the CreateInitialPayload
method. Additionally, I did not want to make a solution that would cause issues on the C# side, so I first searched with GetMethod as usual, and if it was not found, I retrieved all functions with GetMethods(). Leveraging the fact that a dot is included and the method ends with the method name, I created the following extension function:
public static MethodInfo? GetMethodFlex(
this Type type,
string name,
BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public)
{
return type.GetMethod(name,bindingAttr) ?? type.GetMethods().FirstOrDefault(m => m.Name.EndsWith($".{name}"));
}
By doing this, by making extension method of type.GetMethod(
to type.GetMethodFlex(
, it became possible to retrieve both C# and F# cases.
Fortunately, after obtaining the method, we were able to execute it without any issues using the same code, so we described event sourcing in F# and were able to execute it without changing other parts.
Summary
F# allows you to write in a functional style and also supports algebraic data types.
Recently, a proposal regarding Union Type was submitted as a proposal for future language changes in C#.
If this is achieved, it would allow an algebraic data type-like approach in C#, eliminating the need for a default case in a switch statement when all cases are covered. Additionally, the compiler will raise an error when there are missing cases after adding a new item. I am very much looking forward to this, but it is still in the discussion phase and it will likely be implemented in dotnet next year or later.
In the meantime, while studying F#, I would also like to realize better methods for functionally describing domains.
Please feel free to try out Sekiban in either C# or F#. Version 0.20.6, which is compatible with F#, has also been released.
Subscribe to my newsletter
Read articles from Tomohisa Takaoka directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Tomohisa Takaoka
Tomohisa Takaoka
CTO of J-Tech Creations, Inc. Recently working on the development of the event sourcing and CQRS framework Sekiban. Enthusiast of DIY keyboards and trackballs.