Skip to content

Highly efficient compile-time general purpose DI container based on C# source generators.

License

Notifications You must be signed in to change notification settings

lsoft/DpdtInject

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DpdtInject

Dpdt logo

Compilation Status Nuget

Table Of Contents

Purpose

Dpdt is a compile-time general purpose DI container based on C# Source Generators. Its goal is to remove everything possible from runtime and make resolving process as faster as we can. This is achieved by transferring huge piece of resolving logic to the compilation stage into the source generator.

As an additional concept, Dpdt adds no references to your distribution, so if you are developing a library/nuget package, you are free to use Dpdt as DI container. You will not impose your DI to your users, everything will builtin in your dlls.

Status

There is a meduim size project powered by Dpdt that runs in its production. So, I think Dpdt is in Alpha status now.

Design features

  1. As mentioned above, Dpdt suitable for lib/nuget developers. This does not mean that Dpdt is not (or less) suitable for applications.
  2. Dpdt Visual Studio Extension helps you to be more productive.
  3. Additional compile-time checks.
  4. No performance decrease on the platforms with no compilation at runtime (because of absense runtime compilation!).

Other features

  1. Easy-to-read syntax Bind<IA>().To<A>().WithTransientScope().
  2. Custom constructor arguments ... Configure(new ConstructorArgument("message", Message)).
  3. Generic Get<T> and non generic Get(Type t) resolutions.
  4. Constrained GetFast fast resolutions.
  5. Single object Get or collection GetAll resolutions.
  6. Func<T> resolutions.
  7. ToIsolatedFactory bindings with automatic factory code generation.
  8. ToProxy binding with automatic generate an interceptor class.
  9. Transient, singleton and constant scopes.
  10. Custom scopes.
  11. Child kernels (aka child clusters).
  12. Bindings by conventions.
  13. Binding settings which modifies compile-time code generation.
  14. At last, Dpdt produces a very, very fast resolution code (it's the last due to it's really unimportant; every modern DI fast enough for virtually all use cases).

More to come!

Performance

  1. Very impressive Fast resolutions.
  2. Good Generic resolution performance.
  3. Good enough, but not the best NonGeneric resolution - Microresolver is fantastically fast; what's the magic? :)
BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363
AMD Ryzen 7 4700U with Radeon Graphics, 1 CPU, 8 logical and 8 physical cores
.NET Core SDK=5.0.100-rc.2.20479.15
  [Host]     : .NET Core 3.1.7 (CoreCLR 4.700.20.36602, CoreFX 4.700.20.37001), X64 RyuJIT
  Job-OKTIPR : .NET Core 3.1.7 (CoreCLR 4.700.20.36602, CoreFX 4.700.20.37001), X64 RyuJIT

Runtime=.NET Core 3.1  Server=True  

Here is the results of a resolution a complex tree of 500 objects total:

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
Dpdt.GenericSingleton500 4.325 us 0.0407 us 0.0381 us - - - -
Dpdt.NonGenericSingleton500 13.641 us 0.1126 us 0.1054 us - - - -
Dpdt.FastSingleton500 (fastest) 2.577 us 0.0104 us 0.0097 us - - - -
DryIoc.GenericSingleton500 16.148 us 0.1470 us 0.1375 us - - - -
DryIoc.NonGenericSingleton500 9.728 us 0.1689 us 0.1580 us - - - -
Microresolver.GenericSingleton500 7.276 us 0.0573 us 0.0536 us - - - -
Microresolver.NonGenericSingleton500 3.784 us 0.0158 us 0.0148 us - - - -
Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
Dpdt.GenericTransient500 40.32 us 0.330 us 0.309 us 4.1504 - - 77.02 KB
Dpdt.NonGenericTransient500 63.08 us 0.489 us 0.433 us 4.1504 - - 77.02 KB
Dpdt.FastTransient500 (fastest) 37.93 us 0.253 us 0.224 us 3.8452 - - 71.2 KB
DryIoc.GenericTransient500 66.87 us 1.288 us 1.581 us 4.2725 - - 77.02 KB
DryIoc.NonGenericTransient500 62.60 us 0.480 us 0.449 us 4.1504 - - 77.02 KB
Microresolver.GenericTransient500 64.77 us 0.294 us 0.261 us 4.2725 - - 77.02 KB
Microresolver.NonGenericTransient500 68.52 us 2.869 us 8.415 us 4.1504 - - 77.02 KB

Also I recommend disable tiered compilation for composition root assembly if you want to obtain full performance at the start.

How to try

Without Dpdt Visual Studio Extension

  • Create new Console Application in Visual Studio or dotnet console command. Keep in mind you need to set net5 or net6 target framework.
  • Install latest Dpdt.Injector nuget package. Keep in mind that nuget may be in the prerelease state.
  • (optional) You can disable tiered compilation for composition root assembly and set EmitCompilerGeneratedFiles to true.
  • At this point, you have something like the following in your csproj file:
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5</TargetFramework>

    <!-- disable tiered compilation for composition root assembly -->
    <TieredCompilation>false</TieredCompilation>
    <TieredCompilationQuickJit>false</TieredCompilationQuickJit>
    <TieredCompilationQuickJitForLoops>false</TieredCompilationQuickJitForLoops>

    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Dpdt.Injector" Version="0.8.3-alpha" />
  </ItemGroup>

</Project>
  • Next, create a class which will be resolved from a Dpdt container, for example: public class MyPayload { }
  • You will need a Dpdt cluster class:
    public partial class MyCluster : DpdtInject.Injector.Src.DefaultCluster
    {
        [DpdtInject.Injector.Src.DpdtBindingMethod]
        public void Bind()
        {
            Bind<MyPayload>()
                .To<MyPayload>()
                .WithSingletonScope()
                ;
        }
    }
  • Now, it's time to create a cluster and take our payload from it; put the following at Program.Main:
    /*await/* var cluster = new MyCluster(null); //await is needed to dispose singletons with IAsyncDisposable, but without IDisposable interfaces
    var payload = cluster.Get<MyPayload>();
    Console.WriteLine(payload.GetType().Name);
  • Finally, run your program.

With Dpdt Visual Studio Extension

  • Install Dpdt Visual Studio Extension for Visual Studio 2019 or for Visual Studio 2022.
  • Restart Visual Studio.
  • Create new Console Application in Visual Studio. Keep in mind you need to set net5 or net6 target framework.
  • (optional) You can disable tiered compilation for composition root assembly and set EmitCompilerGeneratedFiles to true.
  • Install the latest Dpdt Nuget Package via context menu
  • Create Dpdt cluster and binding method via context menu and the tool window
  • Next, create a class which will be resolved from a Dpdt container, for example: public class MyPayload { }
  • Add the binding clause to the Bind method of the produced cluster:
Bind<MyPayload>()
    .To<MyPayload>()
    .WithSingletonScope()
    ;
  • Now, it's time to create a cluster and take our payload from it; put the following at Program.Main:
    /*await*/ using var cluster = new MyCluster(null); //await is needed to dispose singletons with IAsyncDisposable, but without IDisposable interfaces
    var payload = cluster.Get<MyPayload>();
    Console.WriteLine(payload.GetType().Name);
  • Finally, run your program.

Design

To be fair: design drawbacks at first place

  1. Because of design, it's impossible to Unbind and Rebind.
  2. Because of source generators, it's impossible to direclty debug your bind code, including its When predicates.
  3. Because of massive rewriting the body of the cluster, it's impossible to use a local variables (local methods and other local stuff) in ConstructorArgument and When predicates. To make bind works use instance based fields, properties and methods instead. To make bind debuggable, use fields, properties and methods of the other, helper class.
  4. No deferred bindings by design with exception of cluster hierarchy.
  5. Slower source-to-IL compilation, slower JIT compilation.

Cluster

Dpdt bingings organized in the groups named clusters. Cluster is a class that derived from Dpdt's DpdtInject.Injector.Src.DefaultCluster. This class should be partial. Each cluster may have any numbers of binding methods even in different compilation units. These methods should be marked with attribute [DpdtBindingMethod]. No argument allowed for that methods, and in fact they are not executed at all. You can use this to split your bindings into different groups (something like Ninject's modules).

Cluster life cycle

The life cycle of the cluster begins by creating it with new. The cluster can take other cluster as its parent, so each unknown dependency will be resolved from the parent (if parent exists, otherwise exception would be thrown).

The end of the life cycle of a cluster occurs after the call to its Dispose method. At this point, all of its disposable singleton bindings are also being disposed. It is prohibited to dispose of the cluster and use it for resolving in parallel . It is forbidden to resolve after a Dispose.

If you have at least one singleton with IAsyncDisposable interface, but without IDisposable interface, you need to invoke DisposeAsync instead of Dispose at the cluster object. The rule is simple: Dispose cluster -> Dispose its singletons, AsyncDispose cluster -> Dispose DisposeAsync its singletons. So, I recommend always use DisposeAsync for a cluster objects. Disposing order is undefined.

Child clusters

    public partial class RootCluster : DefaultCluster
    {
        public RootCluster(your arguments) : this((ICluster)null!) { ... }
    }

    public partial class ChildCluster : DefaultCluster
    {
        public ChildCluster(ICluster cluster, your arguments) : this(cluster) { ... }
    }

...

    /*await*/ using var rootCluster = new RootCluster(
        your arguments
        );
    /*await*/ using var childCluster = new ChildCluster(
        rootCluster,
        your arguments
        );

Clusters are organized into a tree. This tree cannot have a circular dependency, since it is based on constructor argument. Dependencies, consumed by the binding in the child cluster, are resolved from the home cluster if exists, if not - from parent cluster.

If some binging does not exist in local cluster, Dpdt will request it from parent cluster at runtime. This behavior can be modified by settings OnlyLocalCluster/AllowedCrossCluster/MustBeCrossCluster.

Child clusters must be disposed BEFORE its parent.

Syntax

Dpdt syntax was partially inspired by Ninject. A lot of examples of allowed syntaxes are available in the test project. Please refer that code.

Regular singleton/transient/custom binding

Bind<IB1, IB2>()
    .To<BClass>()
    .WithSingletonScope() //WithTransientScope, WithCustomScope
    ;

Regular const binding

Only readonly fields, static readonly fields and in-place compile-time constants are allowed to be a target constant:

private /*static*/ readonly string _roString = "readonly string";

...

Bind<string>()
    .WithConstScope(_roString)
    ;


Bind<string>()
    .WithConstScope("some inplace string")
    ;

Conditional bindings

Bind<IA>()
    .WithConstScope(ConstantA)
    .When(rt => rt.ParentTarget?.TargetType == typeof(B2))
    ;

Predefined constructor arguments with additional settings

Bind<IB>()
     .To<B>()
     .WithSingletonScope()
     .Setup<AllowedCrossCluster>()
     .Setup<SubsetNoOrderConstructorSetting<int, long>>() //imagine that argument1 is int, and argument2 is long; note: you are NOT forced to use constructor setting if you are using ConstructorArgument
     .Configure(new ConstructorArgument("argument1", field_or_property_or_expression1))
     .Configure(new ConstructorArgument("argument2", field_or_property_or_expression2))
     ;

Proxy (and decorator at the same time) bindings

//your custom telemetry event saver
//saver is invoked from multiple threads even in parallel
//so saver must be thread-safe
Bind<SessionSaver>()
    .To<SessionSaver>()
    .WithSingletonScope()
    ;

//example of the payload, that will be injected into the proxy:
Bind<ICalculator>()
    .To<Calculator>()
    .WithSingletonScope() //may be singleton or transient
    .When(rt => rt.WhenInjectedExactlyInto<ProxyCalculator>())
    ;


//proxy binding example
//proxy invokes its saver for every invocation of the proxied methods, EVEN in parallel!
Bind<ICalculator>()
    .ToProxy<ProxyCalculator>()
    .WithProxySettings<TelemetryAttribute, SessionSaver>() //additional details about these classes are available at the tests project
    .WithSingletonScope() //proxy should be in the same scope as its payload
    .Setup<SuppressCircularCheck>() //this suppress unused warning
    .When(rt => rt.WhenInjectedExactlyNotInto<ProxyCalculator>()) //this suppress stack overflow during resolution
    ;

//"proto" class of the proxy:
public partial class ProxyCalculator : ICalculator { }

Dpdt proxy can decorate a methods, properties (get/set), events (add/remove) and indexers.

What is saver?

Saver is an interceptor class that is used by the Proxy-decorator to convey some info about the current invocation. Saver MUST be thread-safe!

Saver has 2 methods:

  1. StartSessionSafely
  2. FixSessionSafely

Suffix Safely means that this method should not raise any exception; please wrap their bodies into try-catch.

StartSessionSafely is invoked at the beginning of decorated (proxied) method. Its arguments:

  1. fullClassName contains a full class name (e.g. Dpdt.Injector.DefaultCluster)
  2. memberName contains a method\property\event name, or long this[] for an indexer, where long is a indexer return type
  3. arguments contains an array of member arguments; imagine a method void DoSomething(int a, long b), in this case arguments will contain: name of argument a (e.g. string "a"), value of argument a (e.g. some int), name of argument b (e.g. string "b"), value of argument b (e.g. some long); if no arguments exists arguments may be null

Please make note: parameters with out modifier will not appear in arguments.

StartSessionSafely returns a newly created Guid.

FixSessionSafely is invoked at the end of proxied-method. Its arguments:

  1. sessionGuid contains a Guid that earlier has been returned from StartSessionSafely
  2. takenInSeconds contains a time (a fraction of seconds) that has been spent in decorated member
  3. exception contains an exception in the case if decorated member raises an exception, otherwise null

In the wild, session class usually uses ConcurrentDictionary to store sessions in StartSessionSafely. In FixSessionSafely you can do TryRemove, append the timings and exception, and send this completed session to the logger or whatever you need/want.

Conventional bindings

Conventional bindings are "machine-gun" binding producing machine, that scans your assemblies and produces a lot of binding statements. If you are know Ninject.Conventions you know what I wanted to implement. Conventional bindings works with Roslyn symbols and are produced at compile-time.

ScanInAssembliesWith<A0>() //scan assembly(ies) that contains a specified type(s)
    .SelectAllWith<IA>() //select all classes that implements a specific class/interface
    .ExcludeAllWith<IExclude>() //but not to include all classes that implements this class/interface
    .From<IA, IB>() //binds from these interfaces/classes (they may be different with the classes/interfaces in SelectAllWith); others options are exists too
    .ToItself() //to the processed class
    .WithSingletonScope() //and now we continue with regular binding syntax, including With*Scope/Setup/Configure/When and etc.
    ;

Example how to bind from open generic:

//the following statement will bind A0 and A2; A1 will be excluded due to IExclude<>
ScanInAssembliesWith<A0>()
    .SelectAllWithOpenGeneric<IA<object>>() //generic argument 'object' means NOTHING! it will be removed by Dpdt, so IA<object> will be transformed into IA<> (open generic)
    .ExcludeAllWithOpenGeneric<IExclude<object>>() //again, 'object' means nothing, as above
    .FromAllInterfaces()
    .ToItself()
    .WithSingletonScope()
    ;


public interface IA { }
public interface IA<T1> : IA { }
public interface IExclude<T> { }
public class A0 : IA<object> { }
public class A1 : IA<string>, IExclude<ulong> { }
public class A2 : IA<StringBuilder> { }

Example how to exclude an item from a conventional binding. Imagine you have the following:

//this statement will bind A0 A1 A2 classes
ScanInAssembliesWith<A0>()
    .SelectAllWith<IA>()
    .From<IA>()
    .ToItself()
    .WithSingletonScope()
    ;

but you want to bind A1 in a slightly different way. You can easily do that:

//this statement will bind A0 A2 classes
ScanInAssembliesWith<A0>()
    .SelectAllWith<IA>()
    .ExcludeAllWith<A1>() //only append this
    .From<IA>()
    .ToItself()
    .WithSingletonScope()
    ;

//and add a new binding statement
Bind<IA>()
    .To<A1>()
    .WithTransientScope() //here is the difference: transient instead of singleton
    ;

Dpdt does not support conventional bindings to proxy or factory, because of design.

Choosing constructor

Constructor is chosen at the compilation stage based on 3 principles:

  1. If constructor filtering setting is set, it will be applied against existing constructors.
  2. If any ConstructorArgument filter are set, they will be applied against constructors filtered by previous step. If no ConstructorArgument has defined, all filtered constructors will be taken.
  3. After all filtering, the constructor with the minimum number of parameters is selected to make a binding.

Scope

Bind clause with no defined scope raises a question: an author forgot set a scope or wanted a default scope? We make a decision not to have a default scope and force a user to define a scope.

Singleton

The only one instance of defined type is created. If instance is IDisposable then Dispose method will be invoked at the moment the cluster is disposing. This operation is thread safety, double checking locking algorithm is on.

Transient

Each resolution call results with new instance. Dispose for targets will not be invoked.

Constant

Constant scope is a scope when the cluster receive an outside-created object. Its Dispose will not be invoked, because the cluster is not a parent of the constant object.

Custom

    Bind<IA>()
        .To<A>()
        .WithCustomScope()
        ;

...

    /*await/* using(var scope1 = cluster.CreateCustomScope())
    {
        var a1 = cluster.Get<IA>(scope1);

        /*await/* using(var scope2 = cluster.CreateCustomScope())
        {
            var a2 = cluster.Get<IA>(scope2);
        } //here we dispose a2 if target for IA is IDisposable /*or IAsyncDisposable*/
    } //here we dispose a1 if target for IA is IDisposable /*or IAsyncDisposable*/

IDisposable custom-binded objects will be disposed at the moment of the scope object dispose. DO NOT forget to invoke Dispose on scope object! Otherwise, Custom-scoped disposable objects will not be disposed too.

If you have at least one custom scoped binding with IAsyncDisposable interface, but without IDisposable interface, you need to invoke DisposeAsync instead of Dispose at the scope object. The rule is simple: Dispose scope object -> Dispose its bindings, AsyncDispose scope object -> Dispose DisposeAsync its bindings. So, I recommend always use DisposeAsync for a scope objects. Disposing order is undefined.

Keep in mind, custom-scoped bindings are resolved much slower than singleton/transient/constant bindings.

Conditional binding

Each bind clause may have an additional filter e.g.

    Bind<IA>()
        .To<A>()
        .WithSingletonScope()
        .When(IResolutionTarget rt =>
        {
                condition to resolve
        })
    ;

Please refer unit tests to see the examples. Please note, than any filter makes a resolution process slower (a much slower! 10x slower in compare of unconditional binding!), so use this feature responsibly. Resolution slowdown with conditional bindings has an effect even on those bindings that do not have conditions, but they directly or indirectly takes conditional binding as its dependency. Therefore, it is advisable to place conditions as close to the resolution root as possible.

Also, these predicates cannot be debugged because they are rewrited by Dpdt. See below how to overcome it.

Fast resolutions

Dpdt contains a special resolution type named 'fast'. Its syntax is cluster.GetFast(default(IMyInterface));. In general this syntax is faster that generic resolutions, but it has one additional constraint: you need to resolve directly from cluster type, it is impossible to cast cluster to the one of its interface (like ICluster or IResolution) and do fast resolutions.

Compile-time checks

Each safety checks are processed in the scope of concrete cluster. Dpdt cannot check for cross-cluster issues because clusters tree is built at runtime. But cross-cluster checks are performed in the cluster constructor at runtime.

Did source generators are finished their job?

Dpdt adds a warning to compilation log with the information about how many clusters being processed and time taken. It's an useful bit of information for debugging purposes.

Unknown constructor argument

Dpdt will break ongoing compilation if binding has useless ConstructorArgument clause (no constructor with this parameter exists).

Singleton takes transient or custom (captive dependency)

Dpdt can detect cases of singleton binding takes a transient/custom binding as its dependency, and make signals to the programmer. It's not always a bug, but warning might be useful.

Circular dependencies

Dpdt is available to determine circular dependencies in your dependency tree. In that cases it raise a compilation error. One additional point: if that circle contains a conditional binding, Dpdt can't determine if circular dependency will exists at runtime, so Dpdt raises a compile-time warning instead of error. This behaviour can be changed by appropriate setting.

More than 1 unconditional child

If for some binding more than 1 unconditional child exists it renders parent unresolvable, so Dpdt will break the compilation is that case.

Conventional bindings

Dpdt will write some info in Build log for every conventional binding produced. For regular binding - will not. It is useful for debugging conventional bindings results.

Async resolutions

Dpdt is a constructor-based injector. Async resolutions are not supported because we have no an async constructors. Consider using a factory class with an async method async Task<Something> CreateSomethingAsync(...).

Settings

Settings are things that modify compile-time cluster code generation. THEY ARE NOT WORKING AT RUNTIME.

Cross cluster resolutions

These settings relates with checking for child binding resolutions; they are useful for an additional safety. They are applied for each of child resolutions.

OnlyLocalCluster

Each dependency must exists in local cluster. If not - ongoing compilation will break. Note: binding conditions is out of scope, only existing matters. You can define a local binding with When(rt => false), and this check will mute. So, this setting is not something that can protect you at 100%. This is a default behaiour.

AllowedCrossCluster

Any dependency may be in home cluster or parent cluster. If local dependecy found at runtime it is used, otherwise request to the parent cluster is performed. (this is default behaviour for old version of Dpdt)

MustBeCrossCluster

NO local dependency allowed, any dependency MUST be in the parent cluster. If local dependency found, ongoing compilation will break. Note: binding conditions is out of scope, only existing matters. You can define a local binding with When(rt => false), and this check will fire. So, this setting is not something that can protect you at 100%.

Wrapper producing

These settings relate with a producing of wrapped-resolutions, like Func<>; they are useful for minimizing the cluster size.

NoWrappers

No bindings for wrappers will be produced. It is a default value.

ProduceWrappers

Every type of wrappers will be produced for this binding.

Circular checking

These settings relates with a circular checking; they are useful for removing unused noise from build log (for example in case of decorator, look at ProxySimple0_Fixture unit test).

PerformCircularCheck

Do circular checking. It is a default value.

SuppressCircularCheck

Do not circular checking. Use this for decorators bindings.

Constructor choosing

These settings relates with a constructor choosing algorithm performed by Dpdt. These settings sets a constructor argument types filter.

AllAndOrderConstructorSetting

Dpdt will take the only constructor that have a parameters with the types selected in this setting and in the same order. For example .Setup<AllAndOrderConstructorSetting<int, long>>() means that only constructor MyClass(int a, long b) will choose (argument names does not matters).

SubsetAndOrderConstructorSetting

Dpdt will take the only constructors that have a parameters with the types selected in this setting and in the same order, and additional arguments may exists before or after the selected. For example .Setup<SubsetAndOrderConstructorSetting<int, long>>() will choose the following constructors MyClass(..., int b, long c) MyClass(int a, long b) MyClass(int a, long b, ...).

SubsetNoOrderConstructorSetting

Dpdt will take the only constructors that have a parameters with the types selected in this setting and their order does not matters, any additional arguments may exists. For example .Setup<SubsetNoOrderConstructorSetting<int, long>>() will choose the following constructors MyClass(long a, int b) MyClass(int a, long b) MyClass(..., int a, ... long b, ...) MyClass(..., long a, ..., int b, ...).

Please make note: in readonly and ref modifiers of the constructor arguments will not taken into account.

Debugging your clusters and conditional clauses

Because of source generators are generating new code based on your code, it's impossible to direclty debug your cluster code, including its When predicates (because this code is not actually executed at runtime). It's a disadvantage of Dpdt design. For conditional clauses, you need to call another class to obtain an ability to catch a breakpoint:

    public partial class MyCluster : DefaultCluster
    {
        [DpdtBindingMethod]
        public void BindMethod()
        {
            Bind<IA, IA2>()
                .To<A>()
                .WithSingletonScope()
                .When(rt =>
                {
                    //here debugger is NOT working

                    return Debugger.Debug(rt);
                })
                ;
        }
    }

    public static class Debugger
    {
        public static bool Debug(IResolutionTarget rt)
        {
            //here debugger is working
            return true;
        }
    }

Artifact folder

As a regular source generator, Dpdt is able to store pregenerated C# code at the disk. The only thing you need is correctly setup your csproj. For example:

    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    <!-- next line allows you to define a custom directory to store Dpdt's artifacts, for example: -->
    <!-- <CompilerGeneratedFilesOutputPath>$(ProjectDir)Dpdt.Pregenerated</CompilerGeneratedFilesOutputPath> -->

If your clusters are huge, you may face with slowdowns in your work in VS because VS runs Dpdt in the background. To overcome this please put the following into your csproj:

  <PropertyGroup>
    <Dpdt_Generator_Beautify>false</Dpdt_Generator_Beautify>
  </PropertyGroup>

  <ItemGroup>
    <CompilerVisibleProperty Include="Dpdt_Generator_Beautify" />
  </ItemGroup>

This is to turn off code beautification so Dpdt will produce cluster code a bit faster.

Dpdt Visual Studio Extension

To make a dealing with Dpdt easier, a Visual Studio Extension has been developed. Make note: it only supports Visual Studio 2019 16.8 (and later) and Visual Studio 2022.

Please make note: Dpdt Visual Studio extension should be the SAME version as your Dpdt.Injector package version. If it is not possible, please use the latest version of the both. I carefully try to publish compatible versions of the new nuget and new vsixes at the same time. Sometimes, a new version of vsix can be published without nuget, and the opposite. It means that the new version of vsix (or nuget) will be compatible with last existing version of the nuget (or vsix).

If you click on a project in Solution Explorer and there is no Dpdt nuget installed, you can install its latest version easily:

Dpdt Extension Image 4

Also, you can create a new cluster class or add a new binding method to the existing cluster:

Dpdt Extension Image 5

There is a tool window to look, search and go to any binding in your solution:

Dpdt Extension Image 6

You can enter a text to search everywhere, or use the following predicates to set a search scope:

  • from: to search only in bind from fields
  • to: to search only in bind to field
  • ca: to search only in constructor arguments field
  • other to search only in the scope field
  • location to search only in binding location field

Keep in mind: the searching is case sensitive (like C# too). Examples: MyClass, ca:MyConstructorArgument, location:MyFile.cs etc.

But the main function is to generate a binding clauses through the custom codelens. The following images makes the picture brighter:

Dpdt Extension Image 0

Dpdt Extension Image 1

Dpdt Extension Image 2

Settings window:

Dpdt Extension Image 3

EnableWhitespaceNormalization sets whitespace normalization when you create a new binding through Dpdt Visual Studio Extension.

A lot of thanks to bert2 and his amazing example https://github.com/bert2/microscope, without his microscope no Dpdt extension will appear because of lack of tutorials in the scope of VS extension development.

Known problems

  • Dpdt extension do not support few types (in different assemblies) with the same full name. I will investigate it further.

If any problem occurs with this extension or the generator itself, please let me know. I will need to see the following log files C:\Users\<user>\AppData\Local\Temp\dpdt_*.log.

Alternatives

You may be interesting in the following alternatives:

Feedback

Any ideas for new features of Dpdt and Dpdt Visual Studio Extension are welcome.

Feel free to send a feedback by creating an issues here. Cheers!