1

Let me try to clarify my question with an example. Assume a library of some sort which has a built in logging system and it is published as Library.Core. As the maintainer you want to add a wrapper around your logging system to add support for Microsoft.Extensions.Logging. You create a working implementation and publish it as a new package under the name Library.Extensions.Logging in order to prevent the need for a dependency to Microsoft.Extensions.Logging in your Library.Core package.

Often such implementations are just a couple lines of boilerplate code and at least in my opinion, it feels like unnecessary to create a new package for such little code.

The main problem really is, that you would need to reference Microsoft.Extensions.Logging, just for an optional feature. However, back to the example.

With the introduction of Source Generators you could add the code inside of Library.Extensions.Logging to a SG which is being shipped with Library.Core. Furthermore, you could generate the wrapper code only if the target assembly references Microsoft.Extensions.Logging.

This works fairly well in practice, however is it a good practice?

PS: When I say Source Generators, I am specifically referring to the new C#9 Source Generators.

0

3 Answers 3

4

Half an hour ago I read an article about the new C#9 features with an example of C# Source Generators. This is definitely an interesting feature, and having worked with several different kind of code generators over the years, I don't think there is anything inherently wrong when using it for the purpose of creating code which depends optional dependencies. Quite the opposite, it seems to be a fine example of what a Source Generator can do for you - to enable or adapt things in code which can only be decided about at compile time.

I would, however, care for some caveats. First, the increased testing effort: to make sure the lib will work with or without the reference to Microsoft.Extensions.Logging correctly, you will have to test against both scenarios. For example, for automated unit tests, this means you will have to create two different unit test projects, one "standard" project with most of the tests, and one which contains the optional reference and some additional tests. Note this is not quite different when you implement your extension in a separate extension package.

However, and maybe of more practical relevance: since the generated code is created dynamically from strings, it does usually not work well with the refactoring tools from your IDE, not work well with syntax checks by the IDE during editing, and not work well with Intellisense. So, for example, when you rename a function in your core library using the "rename refactoring" from Visual Studio, it won't rename any calls in the generated code automatically, you will have to care for this "manually".

Hence I would recommend to keep the generated code sections as small as possible. This won't eliminate the need for testing the different scenarios, but can minimize the risk of introducing bugs into working code by modifications through the generator.

7
  • What would you think about the following approach. Still generate such things with Source Generators, however also publishing the same code as a package for legacy purposes.
    – Twenty
    Commented May 10, 2021 at 20:35
  • @Twenty: by "legacy purposes", you mean enabling support for C# prior to version 9? If you can generate this from a single source automatically, why not? However, I would only go that route when I know for sure there are clients for the lib which demand such support.
    – Doc Brown
    Commented May 10, 2021 at 20:43
  • Yea I was referring to C# versions prior 9. Thanks for the advice though!
    – Twenty
    Commented May 10, 2021 at 21:49
  • @Twenty: I am actually not sure if the code generated by "C#9 Source Generators" can be captured easily for publishing. With a self-made code generator, that would be simple, but I lack experience with this new feature so far.
    – Doc Brown
    Commented May 11, 2021 at 6:54
  • 1
    @Twenty: I found another caveat, see my last edit.
    – Doc Brown
    Commented May 12, 2021 at 5:23
2

You're mixing a few concepts here.

You've got class wrappers, you've got code generators, you've got build-on-deploy. The solution you're talking about works if you can safely build the application per deployment, which is a big assumption.

A different architectural approach might be to making your Logging feature an interface, and allowing any all implementations of that interface, and perhaps scanning for implementations of that interface at runtime.

Here's an example of how you might make that work using Reflection:

https://stackoverflow.com/questions/26733/getting-all-types-that-implement-an-interface

In terms of deployment, you're saying something like "This app logs to anything that implements IMyLog, so if you want logging to work make sure you drop a DLL in the runtime directory that has an IMyLog in it."

My point is that you seem to be solving a deployment problem with a complicated workaround that might force you to compile the application individually for each installation. I'm suggesting that you consider solving the deployment problem at the point of deployment instead.

5
  • 1
    I do get your point, however this would still require the "user" of the library to write an implementation on their own, which isn't really what I am aiming for. What I am really trying to solve is the issue with optional dependencies. I do want to provide an implementation for the consumer, but it feels weird to create extra packages with such small amounts of code.
    – Twenty
    Commented May 10, 2021 at 17:49
  • You can provide a default implementation in your main application and fall back on it if the user doesn't supply their own. Or... what is the problem you're trying to solve? If you don't want to ship these "options" with your product, and you don't want the user to supply them, what exactly is the deployment model here? Put into concise CS-type terms, you're trying to implement compile-time binding and I'm calling for run-time binding. If you really want to bake the "options" into the build, consider conditional compilation with #if/#endif.
    – catfood
    Commented May 10, 2021 at 22:10
  • 2
    I am not sure if you really understood the scenario of the OP. They publish a library as source code for usage by other developers. The library contains an optional feature which usually would require a reference to another assembly, even if that optional feature is not used. This could prevent the usage of the lib by devs who don't need this optional feature, because the additional reference could causes some issues in their environment. ...
    – Doc Brown
    Commented May 11, 2021 at 10:54
  • 1
    .. Putting the additional code section into a generator block, however, allows to activate the code only in environments where the additional dependency is already there, so causing no problems. The alternative would be to make an additional "extensions" library to the basic library, where only the extensions library holds the additional reference. That works, too, but may cause more management overhead when the optional feature is a only a small extension.
    – Doc Brown
    Commented May 11, 2021 at 10:54
  • You're right, I didn't understand the scenario. Always start by defining the problem you are trying to solve!
    – catfood
    Commented May 11, 2021 at 13:52
-1

A library suddenly changing its behavior depending on whether some seemingly unrelated library is referenced feels far too magical to me. I would avoid a design like this.

If I want Library to log, I can use the package that explicitly says "this will make Library log".

6
  • 1
    What you wrote is not wrong, but I am pretty sure the OP did not mean to change the behaviour of the core library automatically under the hood when an additional dependency is added. The scenario scetched looks more like providing an extension feature which can only be used (transparently) in presence of that extra dependency.
    – Doc Brown
    Commented May 12, 2021 at 5:38
  • @DocBrown The OP is somewhat ambiguous, but this is how I understand "a SG which is being shipped with Library.Core. Furthermore, you could generate the wrapper code only if the target assembly references Microsoft.Extensions.Logging." Commented May 12, 2021 at 7:04
  • @DocBrown is actually quite on spot. I am not trying to change behavior. I am trying to add behavior, if a given reference to a library is found.
    – Twenty
    Commented May 12, 2021 at 9:42
  • Eh, I think there's not that much difference between adding behavior and changing it. The latter is worse, of course, but it's still very surprising. Commented May 12, 2021 at 10:00
  • @SebastianRedl: of course, any language feature which enables things which were not possible beforehand may surprise devs because they are new. For example, lots of devs were sceptical about type inference and its the "magic" behind the var keyword in C# when they were introduced. That does not mean one should avoid using such features, but one should learn how to use them properly.
    – Doc Brown
    Commented May 12, 2021 at 11:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.