Note/Disclaimer: After a few searches, the nearest thing I have I have seen to this post is a post on SO (Method chaining and the finishing problem) which is similar to my question, but doesn't really answer it - but anyway, I hope this is not a duplicate question.
What I am doing:
I have created a fluent interfaceas a facade over an existing logging framework for a bunch of method calls - so my syntax looks a bit like this:
Logger.Debug().Message("Debug message!").WriteToLog();
Logger.Error().Message("An exception occured").Exception(ex).WriteToLog();
I am passing an internal object from one method call to the next object so that when the final call is made (the WriteToLog method); the message is written to a log file somewhere.
The bit I think smells
In order to verify (only when the application is built in debug mode), I have a property on a context class (just a property bag object) which gets passed from method call to the returned object until the chain terminates; it is a boolean and defaults to false.
This property is evaluated in the context class destructor using a Debug.Assert to determine if the final method to end the chain is called so any logging errors can be picked up during development. (the property, the code which sets the property and the destructor itself are all created in the context of a #if DEBUG pre-processor directive, so if it is built in release or if the symbol doesn't exist, the code will not get compiled.)
I know using a destructor is bad in c#2.0 and above, and that I may not have access to properties because I believe there are no guarantees about the finalization order. This is why it only happens when built in Debug mode, and why I would like to get away from it.
The reason I am trying to build an assertation in is because it is very easy to forget and end up writing code like
Logger.Debug().Message("Debug message!");
which means that nothing gets logged, though at a cursory glance it looks like it should.
My Question
What I want to know is - can anyone think of another way of verifying that the final method is always called? These messages are just required during development to highlight to the developer that a method chain hasn't finished - I don't want the end users finding error messages related to logging in the end product.
Method chaining, also known as named parameter idiom, is a common syntax for invoking multiple method calls in object-oriented programming languages. Each method returns an object, allowing the calls to be chained together in a single statement without requiring variables to store the intermediate results.
Method chaining is a programmatic style of invoking multiple method calls sequentially with each call performing an action on the same object and returning it. It eliminates the cognitive burden of naming variables at each intermediate step.
Method Chaining is the practice of calling different methods in a single line instead of calling other methods with the same object reference separately. Under this procedure, we have to write the object reference once and then call the methods by separating them with a (dot.).
Method chaining is a technique that is used for making multiple method calls on the same object, using the object reference just once. Example − Assume we have a class Foo that has two methods, bar and baz. We create an instance of the class Foo − foo = Foo()
First of all I would question the need for a fluent interface in this case at all, seems you can easily get by with a much simpler interface:
Logger.Debug.Message("Test");
or even just:
Logger.Debug("Test");
However, if you really need/want a fluent interface, a different way to do this would be to make the fluent interface work on a parameter to the method, instead of upon the return value.
So instead of doing this:
Method1().Method2().Method3();
and then forgetting the final call:
Method1().Method2().Method3().Execute();
you would instead organize the code, perhaps like this:
Method1(o => o.Method2().Method3());
To do this, you would define an object upon which you will call all the fluent methods:
public class LoggerOptions
{
public LoggerOptions Debug() { LoggerType = LoggerType.Debug; return this; }
public LoggerOptions Error() { LoggerType = LoggerType.Error; return this; }
public LoggerOptions Message(string message) { ...; return this; }
public LoggerType Type { get; set; }
...
}
Every method call here would modify the LoggerOptions object, and then return the same instance back, to continue the fluent interface.
and then:
public static class Logger
{
public static void Log(Func<LoggerOptions, LoggerOptions> options)
{
LoggerOptions opts = options(new LoggerOptions());
// do the logging, using properties/values from opts to guide you
}
}
You would then call it like this:
Logger.Log(opts => opts.Debug().Message("Debug message"));
If you have some terminal methods you absolutely need to call before finalizing setting up the options object, you can make different objects:
public class LoggerOptions
{
public LoggerOptions Debug() { LoggerType = LoggerType.Debug; return this; }
public LoggerOptions Error() { LoggerType = LoggerType.Error; return this; }
public LoggerOptions Message(string message) { ...; return this; }
public LoggerType Type { get; set; }
...
public LoggerFinalOptions ToEventLog() { ...; return new LoggerFinalOptions(this); }
public LoggerFinalOptions ToFile(string fileName) { ...; return new LoggerFinalOptions(this); }
}
and then:
public static class Logger
{
public static void Log(Func<LoggerOptions, LoggerFinalOptions> options)
{
LoggerFinalOptions opts = options(new LoggerOptions());
// do the logging, using properties/values from opts to guide you
}
}
This would then guarantee that you could not compile the code without ending the chain of methods with a call to something that returns the explicit final options object:
// will not compile
Logger.Log(opts => opts.Debug().Message("Test"));
// will compile
Logger.Log(opts => opts.Debug().Message("Test").ToFile("log.log"));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With