Coder Social home page Coder Social logo

pakrym / jab Goto Github PK

View Code? Open in Web Editor NEW
1.0K 22.0 33.0 490 KB

C# Source Generator based dependency injection container implementation.

License: MIT License

C# 99.11% PowerShell 0.89%
singleton-service dependency-injection source-generators roslyn roslyn-generator microsoft-extensions

jab's Introduction

Jab Compile Time Dependency Injection

Nuget

Jab provides a C# Source Generator based dependency injection container implementation.

  • Fast startup (200x faster than Microsoft.Extensions.DependencyInjection). Details.
  • Fast resolution (7x faster than Microsoft.Extensions.DependencyInjection). Details.
  • No runtime dependencies.
  • AOT and linker friendly, all code is generated during project compilation.
  • Clean stack traces:
    stacktrace
  • Readable generated code:
    generated code
  • Registration validation. Container configuration issues become compiler errors:
    generated code
  • Incremental generation, .NET 5/6/7/8 SDK support, .NET Standard 2.0 support, Unity support

Example

Add Jab package reference:

<ItemGroup>
    <PackageReference Include="Jab" Version="0.10.2" PrivateAssets="all" />
</ItemGroup>

Define a service and implementation:

internal interface IService
{
    void M();
}

internal class ServiceImplementation : IService
{
    public void M()
    {
    }
}

Define a composition root and register services:

[ServiceProvider]
[Transient(typeof(IService), typeof(ServiceImplementation))]
internal partial class MyServiceProvider { }

Use the service provider:

MyServiceProvider c = new MyServiceProvider();
IService service = c.GetService<IService>();

Features

  • No runtime dependency, safe to use in libraries
  • Transient, Singleton, Scoped service registration
  • Named registrations
  • Factory registration
  • Instance registration
  • IEnumerable resolution
  • IDisposable and IAsyncDisposable support
  • IServiceProvider support

The plan is to support the minimum feature set Microsoft.Extensions.DependencyInjection.Abstraction requires but NOT the IServiceCollection-based registration syntax as it is runtime based.

Singleton services

Singleton services are created once per container lifetime in a thread-safe manner and cached. To register a singleton service use the SingletonAttribute:

[ServiceProvider]
[Singleton(typeof(IService), typeof(ServiceImplementation))]
internal partial class MyServiceProvider { }

Singleton Instances

If you want to use an existing object as a service define a property in the container declaration and use the Instance property of the SingletonAttribute to register the service:

[ServiceProvider]
[Singleton(typeof(IService), Instance = nameof(MyServiceInstance))]
internal partial class MyServiceProvider {
    public IService MyServiceInstance { get;set; }
}

Then initialize the property during the container creation:

MyServiceProvider c = new MyServiceProvider();
c.MyServiceInstance = new ServiceImplementation();

IService service = c.GetService<IService>();

Named services

Use the Name property to assign a name to your service registrations and [FromNamedServices("...")] attribute to resolve a service using its name.

[ServiceProvider]
[Singleton(typeof(INotificationService), typeof(EmailNotificationService), Name="email")]
[Singleton(typeof(INotificationService), typeof(SmsNotificationService), Name="sms")]
[Singleton(typeof(Notifier))]
internal partial class MyServiceProvider {}

class Notifier
{
    public Notifier(
        [FromNamedServices("email")] INotificationService email,
        [FromNamedServices("sms")] INotificationService sms)
    {}
}

NOTE: Jab also recognizes the [FromKeyedServices] attribute from Microsoft.Extensions.DependencyInjection.

Factories

Sometimes it's useful to provide a custom way to create a service instance without using the automatic construction selection. To do this define a method in the container declaration and use the Factory property of the SingletonAttribute or TransientAttribute to register the service:

[ServiceProvider]
[Transient(typeof(IService), Factory = nameof(MyServiceFactory))]
internal partial class MyServiceProvider {
    public IService MyServiceFactory() => new ServiceImplementation();
}

MyServiceProvider c = new MyServiceProvider();
IService service = c.GetService<IService>();

When using with TransientAttribute the factory method would be invoked for every service resolution. When used with SingletonAttribute it would only be invoked the first time the service is requested.

Similar to constructors, factories support parameter injection:

[ServiceProvider]
[Transient(typeof(IService), Factory = nameof(MyServiceFactory))]
[Transient(typeof(SomeOtherService))]
internal partial class MyServiceProvider {
    public IService MyServiceFactory(SomeOtherService other) => new ServiceImplementation(other);
}

Scoped Services

Scoped services are created once per service provider scope. To create a scope use the CreateScope() method of the service provider. Service are resolved from the scope using the GetService<IService>() call.

[ServiceProvider]
[Scoped(typeof(IService), typeof(ServiceImplementation))]
internal partial class MyServiceProvider { }

MyServiceProvider c = new MyServiceProvider();
using MyServiceProvider.Scope scope = c.CreateScope();
IService service = scope.GetService<IService>();

When the scope is disposed all IDisposable and IAsyncDisposable services that were resolved from it are disposed as well.

Generic registration attributes

You can use generic attributes to register services if your project targets net7.0 or net6.0 and has LangVersion set to preview.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFrameworks>net7.0</TargetFrameworks>
  </PropertyGroup>

</Project>

Generic attributes allow declaration to be more compact by avoiding the typeof calls:

[ServiceProvider]
[Scoped<IService, ServiceImplementation>]
[Import<IMyModule>]
internal partial class MyServiceProvider { }

Modules

Often, a set of service registrations would represent a distinct set of functionality that can be included into arbitrary service provider. Modules are used to implement registration sharing. To define a module create an interface and mark it with ServiceProviderModuleAttribute. Service registrations can be listed in module the same way they are in the service provider.

[ServiceProviderModule]
[Singleton(typeof(IService), typeof(ServiceImplementation))]
public interface IMyModule
{
}

To use the module apply the Import attribute to the service provider type:

[ServiceProvider]
[Import(typeof(IMyModule))]
internal partial class MyServiceProvider
{
}

MyServiceProvider c = new MyServiceProvider();
IService service = c.GetService<IEnumerable<IService>>();

Modules can import other modules as well.

NOTE: module service and implementation types have to be accessible from the project where service provider is generated.

Root services

By default, IEnumerable<...> service accessors are only generated when requested by other service constructors. If you would like to have a root IEnumerable<..> accessor generated use the RootService parameter of the ServiceProvider attribute. The generator also scans all the GetService<...> usages and tries to all collected type arguments as the root service.

[ServiceProvider(RootServices = new [] {typeof(IEnumerable<IService>)})]
[Singleton(typeof(IService), typeof(ServiceImplementation))]
[Singleton(typeof(IService), typeof(ServiceImplementation))]
[Singleton(typeof(IService), typeof(ServiceImplementation))]
internal partial class MyServiceProvider
{
}

MyServiceProvider c = new MyServiceProvider();
IService service = c.GetService<IEnumerable<IService>>();

Samples

Console application

Sample Jab usage in console application can be found in src/samples/ConsoleSample

Performance

The performance benchmark project is available in src/Jab.Performance/.

Startup time

The startup time benchmark measures time between application startup and the first service being resolved.

| Method |        Mean |     Error |    StdDev |  Ratio | RatioSD |  Gen 0 |  Gen 1 | Gen 2 | Allocated |
|------- |------------:|----------:|----------:|-------:|--------:|-------:|-------:|------:|----------:|
|   MEDI | 2,437.88 ns | 14.565 ns | 12.163 ns | 220.91 |    2.72 | 0.6332 | 0.0114 |     - |    6632 B |
|    Jab |    11.03 ns |  0.158 ns |  0.123 ns |   1.00 |    0.00 | 0.0046 |      - |     - |      48 B |

GetService

The GetService benchmark measures the provider.GetService<IService>() call.

| Method |      Mean |     Error |    StdDev | Ratio | RatioSD |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|------- |----------:|----------:|----------:|------:|--------:|-------:|------:|------:|----------:|
|   MEDI | 39.340 ns | 0.2419 ns | 0.2263 ns |  7.01 |    0.09 | 0.0023 |     - |     - |      24 B |
|    Jab |  5.619 ns | 0.0770 ns | 0.0643 ns |  1.00 |    0.00 | 0.0023 |     - |     - |      24 B |

Unity installation

  1. Navigate to the Packages directory of your project.
  2. Adjust the project manifest file manifest.json in a text editor.
  3. Ensure https://registry.npmjs.org/ is part of scopedRegistries.
  4. Ensure com.pakrym is part of scopes.
  5. Add com.pakrym.jab to the dependencies, stating the latest version.

A minimal example ends up looking like this:

{
  "scopedRegistries": [
    {
      "name": "npmjs",
      "url": "https://registry.npmjs.org/",
      "scopes": [
        "com.pakrym"
      ]
    }
  ],
  "dependencies": {
    "com.pakrym.jab": "0.10.2",
    ...
  }
}

Debugging locally

Run dotnet build /t:CreateLaunchSettings in the Jab.Tests directory would update the Jab\Properties\launchSettings.json file to include csc invocation that allows F5 debugging of the generator targeting the Jab.Tests project.

jab's People

Contributors

alontalmi avatar andreytretyak avatar codito avatar ettud avatar github-actions[bot] avatar michaelsimons avatar notanaverageman avatar pakrym avatar skarllot avatar viktorhofer avatar wieslawsoltes avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

jab's Issues

Current project status?

Of the three main source generator-based dependency injection frameworks (Jab, StrongInject and Pure.DI), Jab's API and generated code are, in my opinion, by far the cleanest and most comprehensible.

However, while the other two projects are very active (but still in need of significant further refinement), Jab is very usable in its current state, but I note that there hasn't been much recent development. Consequently, I was wondering what the status of the project was. For example, Is it still planned to continue to develop Jab and bring it out of beta stage? Or is Microsoft planning any reference implementation of DI container based on source generators?

By way of background, I am currently implementing a source generator to implement a CQRS mediator, together with event logging and replay (and hopefully background event processing in future), and am trying to ascertain whether it is sensible to prepare an implementation of the generator tied to one or more of the source generator-based dependency injection frameworks.

Incompatibility with Microsoft.Extensions.DependencyInjection

Hi

When using Jab in a project that also has Microsoft.Extensions.DependencyInjection installed you get errors like
ServiceProvider.Generated.cs(34,63,34,91): error CS0121: The call is ambiguous between the following methods or properties: 'Jab.ServiceProviderOfTExtensions.GetService<T>(Jab.IServiceProvider<T>)' and 'Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService<T>(System.IServiceProvider)'

You don't necessarily have to install Microsoft.Extensions.DependencyInjection directly. For example EF Core has a dependency on it so if you need EF there is no way around it.

Memory leak issue caused by IDisposable (?)

I am trying to understand the generated code and found out that there is List<object>? _disposables; which keeps the IDisposable object from the service, but I think that also causes the object to leak(?), here is the test to check if the object is leaking:

var p = new MyServiceProvider();
WeakReference w = new(null);
var i = new Action(() =>
{
    var service = p.GetService<IService>();
    w.Target = service;
});
i();
GC.Collect();
Console.WriteLine($"Is the object still alive? {w.IsAlive}");

If the service doesn't implement IDisposable the object didn't leak, but if it does it leak

My use case is that the provider is alive as long as the app still running, I am using dotnet android

WPF build failed

Hi,

I have been testing this project for wpf application but failed to build the app with an error "The type or namespace name 'SingletonAttribute' could not be found (are you missing a using directive or an assembly reference?)" even though roslyn analyzer found no error before building the project.

Maybe I have wrong configuration or something. This is a sample project I've created sample project. Hoping for someone who can enlighten me :)

Thanks a lot!

Generic Services

Hi, thank you so much for fixing those other two issues so incredibly fast @pakrym!

I'm not quite sure what is going on with this issue, I might need to investigate more.

I use ILogger<> in multiple classes.

class MyService(ILogger<MyService> logger) { }
class MyOtherService(ILogger<MyOtherService> logger) { }

[ServiceProvider]
[Singleton(typeof(ILogger<>), typeof(Logger<>))]
[Singleton(typeof(MyService))]
[Singleton(typeof(MyOtherService))]
partial class ServiceContainer { }

This does not work, as it tries to insert the same ILogger<MyService> into the MyOtherService constructor.

I tried helping Jab by setting the needed services as rootServices but to no success:

[ServiceProvider(RootServices = new [] {
    typeof(ILogger<MyService>), 
    typeof(ILogger<MyOtherService>) })]

I saw that you have some functional tests the cover generic services, but this case does not seem to be covered yet.

Thank you very much for this cool project, I really love the clean Stacktraces from using Jab.

Creating lazy type-safe factories for safe runtime service provision

Problem

I have the scenario where I want to decide at runtime which service to inject.

public interface IExampleService;
public class ExampleServiceA : IExampleService;
public class ExampleServiceB : IExampleService;

I see different solutions for this right now:

Use injected IServiceProvider

[ServiceProvider]
[Singleton<IExampleService>(Factory = nameof(ChooseExampleService))]
[Singleton<ExampleServiceA>] // No service for type 'releaser.Lib.ExampleServiceA' has been registered.
[Singleton<ExampleServiceB>] // No service for type 'releaser.Lib.ExampleServiceB' has been registered.
public partial class ExampleProvider(bool useA) {
    public IExampleService ChooseExampleService(IServiceProvider provider) => useA
        ? provider.GetRequiredService<ExampleServiceA>()
        : provider.GetRequiredService<ExampleServiceB>();
}

โš ๏ธ This works as expected, unless I do not register the example services using the attributes, which will result in runtime errors.

Use pre-generated instances

[ServiceProvider]
[Singleton<IExampleService>(Factory = nameof(ChooseExampleService))]
[Singleton<ExampleServiceA>]
[Singleton<ExampleServiceB>]
public partial class ExampleProvider(bool useA) {
    public IExampleService ChooseExampleService(ExampleServiceA a, ExampleServiceB b) => useA ? a : b;
}

โš ๏ธ This also works, with the benefit of being type-safe, but eager evaluated. Missing the service registrations

Proposed solution

Using your IServiceProvider<T> interface or other wrappers to lazily create instances of the service. 1

Edit: maybe using something like Lazy<T> is better suited for this purpose.

[ServiceProvider]
[Singleton<IExampleService>(Factory = nameof(ChooseExampleService))]
[Singleton<ExampleServiceA>]
[Singleton<ExampleServiceB>]
public partial class ExampleProvider(bool useA) {
    internal IExampleService ChooseExampleService(IServiceProvider<ExampleServiceA> aProvider, IServiceProvider<ExampleServiceB> bProvider) => useA
        ? aProvider.GetService()
        : bProvider.GetService();
}

โœ… This allows Jab to gather the necessary information on service dependencies beforehand (i.e. in this case IExampleService depends on ExampleServiceA and ExampleServiceB during runtime), which is lost when using the plain IServiceProvider interface.
This could be a minimally invasive approach to solve this problem.

Let me know what you think of this idea. If you think this is out of scope, feel free to close this issue, it shall just be my suggestion. I just thought it might be fitting with the whole "build-time checking" and source generation aspects of this project.

Footnotes

  1. Note that the factory currently has to be internal at most, because of the accessibility level of the IServiceProvider<T>. โ†ฉ

Support class modules and Import(Type, string Factory)

It would be great if a module could be defined as a class so that it can have a constructor which is useful when passing in Func factory methods to the module. In the provider itself, the module would then be declared as [Import(typeof(X), Factory="ModuleFactory")].

Just in theory, is that possible? What was the reason for choosing an interface over a class for modules?

Expose multiple interfaces of the same instance

Hi! This project looks very cool from my first experiments with it. Thank you very much for all your effort.

I have this problem: My service implements two interfaces and both of them need to be used for dependency injection and both should return the same instance.

Basically like this example (taken from here)

public interface IBar {}
public interface IFoo {}

public class Foo : IFoo, IBar {}

in MEDI this can be accomplished like this (taken from the same source):

services.AddSingleton<Foo>(); // We must explicitly register Foo
services.AddSingleton<IFoo>(x => x.GetRequiredService<Foo>()); // Forward requests to Foo
services.AddSingleton<IBar>(x => x.GetRequiredService<Foo>()); // Forward requests to Foo

Can this already achieved somehow in Jab? I tried to create a Factory method, but it does not seem to work correctly. I believe I am still receiving separate instances.

[ServiceProvider]
[Scoped(typeof(Foo))]
[Scoped(typeof(IFoo), Factory = nameof(IFooFactory))]
[Scoped(typeof(IBar), Factory = nameof(IBarFactory))]
partial class ServiceContainer
{
    public IBar IBarFactory() => GetService<Foo>();
    public IFoo IFooFactory() => GetService<Foo>();
}

Two IEnumerable collide

Hi,
My generated container has two separate IEnumerable sets of services. It looks like Jab tries to create two (internal) properties named "_IEnumerable" both with the same name.

I'm getting this error message:

ServiceContainer.Generated.cs(115,98,115,110): error CS0102: The type 'ServiceContainer' already contains a definition for '_IEnumerable'

I will look further into this issue tomorrow.

Generic attributes in .NET 7

Hi, i'm using jab in a .NET 7 project with preview lang version (upgraded from .NET 6) but i can't use generic attributes anymore.

Use Interlocked.CompareExchange instead of locking

The generated code uses a lock to update a stored service. Have you considered Interlocked.CompareExchange. It would avoid the context switch the lock introduces and might be faster.

if (_someService == default)
    Interlocked.CompareExchange(ref _someService, default, new MyNamespace.SomeService());
return _someService;

Rider is unable to resolve Jab namespace

When creating a fresh .NET 6 console application in Rider and installing Jab 0.8.3, all references to Jab are unable to be resolved. This prevents the usage of attributes such as [ServiceProvider] making this library annoying to use for a Rider developer.

I've tested this with .NET 6.0.300, 6.0.400, preview 7 of .NET 7, on both Linux & Windows but had no luck. The interesting part is that building the project with dotnet build or via Rider's build function, the project builds fine. It appears then to be an IDE-specific issue with discovering the Jab namespace or the likes.

On my work laptop I've been able to reproduce the issue with Rider and loading the same project with VS 2022 works fine.

The duality of IDE & build:
image

Registration by convention

I know at this point you are working through core features, stability and things of that nature. One thought / question that came up when looking at this project is will it be possible to support some kind of convention based registration ? With a traditional IoC container convention based registration comes with a time cost. scanning assemblies for types and doing type checks is expensive. This means application start takes longer. A compile time IoC can solve this issue if it supported such a feature.

Again. I know this is an advanced feature and there are other priorities at this point but wanted to plant the seed of what I think would be a game changing feature.

PackageVersion is missing from the repository

I would like to use Jab in the .NET SDK as the dependency injection provider. As we have build from source requirements, we can't rely on prebuilt NuGet packages. Instead I plan to add this repo as a git submodule to our external source build repo. Everything works as expected, the only thing that seems missing is the PackageVersion information.

@pakrym I assume that you manually pass in the PackageVersion before you publish the package? Can we add the PackageVersion to the Jab.csproj so that a commit corresponds to a given package version, expressed in source? When I build the repo myself I get the following package Jab.0.5.3-beta.1.nupkg instead of Jab.0.8.0.nupkg, which is the latest release on nuget.org.

Register both as service and implementation

When a service is registered as a base type, I want it to also be registered as self so that I can resolve it both via the base type and the self type.

This can be achieved with a RegisterAsSelf bool parameter in attributes. If you can direct me where to make the changes in generator code I can send a PR.

IServiceProvider inject

I'm making a console application that has commands. Commands are classes implementing an ICommand interface. All the commands are registered with the ICommand interface. I would like to create a help command (it also implements the ICommand interface) that requests registered ICommand-s. Of course this is a dependency cycle, because help command requires itself.
Normally I would inject the service provider and resolve the commands in a "lazy" way when calling the command.

Is there any way to inject IServiceProvider<IEnumerable<ICommand>> into the help command?

Factories for generic services are ignored

Code for reproduction:

        internal interface IService<T>{} 
        class ServiceImplementation<T> : IService<T> { public ServiceImplementation(T innerService) {        } }
        interface IService1 {}
        class ServiceImplementation : IService1 { }

        [ServiceProvider]
        [Singleton(typeof(IService<>), typeof(ServiceImplementation<>), Factory = nameof(Cr))]
        [Singleton(typeof(IService1), typeof(ServiceImplementation))]
        partial class Container
        {
            private IService<T> Cr<T>()
            {
                return new ServiceImplementation<T>(default);
            }
        }

Expected result:

private partial class Container: //...

//...
            JabTests.IService<JabTests.IService1> IServiceProvider<JabTests.IService<JabTests.IService1>>.GetService()
            {
                if (_IService_IService1 == default)
                lock (this)
                if (_IService_IService1 == default){
                    _IService_IService1 = Cr<JabTests.IService1>();
                }
                return _IService_IService1;
            }
//...

Actual result:

private partial class Container: //...
           IServiceProvider<JabTests.IService<JabTests.IService1>>
        {
//...
            JabTests.IService<JabTests.IService1> IServiceProvider<JabTests.IService<JabTests.IService1>>.GetService()
            {
                if (_IService_IService1 == default)
                lock (this)
                if (_IService_IService1 == default){
                    _IService_IService1 = new JabTests.ServiceImplementation<JabTests.IService1>(this.GetService<JabTests.IService1>());
                }
                return _IService_IService1;
            }
//...
        }

As you can see, the defined Factory is ignored without any warnings and the constructor is used.

Auto register `IServiceScopeFactory`

Jab could register automatically IServiceScopeFactory allowing to get it using GetService method.

Currently I need to register manually, as follows:

[ServiceProvider]
[Transient(typeof(IServiceScopeFactory), Factory = nameof(CreateScopeFactory))]
public sealed partial class MyContainer
{
    private IServiceScopeFactory CreateScopeFactory() => this;
}

This allows using CreateScope extension method.

var scope = ((IServiceProvider)new MyContainer()).CreateScope();

Constructor from other part of a partial class is not detected and produces invalid code.

Hello, I'm trying to use Jab for my current project for which I previously used AutoCtor to auto-generate DI constructors for my service implementations. Now I want to use Jab to automatically create a service provider from these constructors, but i get invalid source code without any jab warning.

I produced a minimal example:

using AutoCtor;
using Jab;

namespace JabTest;
[ServiceProvider]
[Singleton<Service1>]
[Singleton<Service2>]
public partial class Container { }

[AutoConstruct]
public partial class Service1 {
    private readonly Service2 subservice;
}

public class Service2 { }

The AutoConstruct marker generates the following code:

namespace JabTest;
partial class Service1 {
    public Service1(Service2 subservice) => this.subservice = subservice;
}

However the IServiceProvider<Service1> implementation was generated with an invalid constructor.

partial class Container : IServiceProvider<Service1> {
    private Service1? _Service1;
    Service1 IServiceProvider<Service1>.GetService() {
        if (_Service1 == default)
            lock (this)
                if (_Service1 == default) {
                    _Service1 = new Service1();
                }
        return _Service1;
    }
}

This code detected the partial class without any constructors and assumes there is no specific constructor and uses a nonexistent constructor without arguments. This code raises the compiler error: CS7036 There is no argument given that corresponds to the required parameter 'subservice' of 'Service1.Service1(Service2)'.

I'm not very familiar with source generation, and I've only programmed a very simple one myself once. I think the reason might be that the Jab SG executes before the AutoCtor SG does and wont detect any of it's generated code, or maybe it just ignores the fact that it is a partial class (which would likely be an easier fix).

Build errors after reinstalling VS2019

I have integrated Jab into my Xamarin Forms project. It worked very well until I had to reinstall VS2019 some days ago.
Since reinstalling I am getting build errors:

CS0246 The type or namespace name 'Jab' could not be found (are you missing a using directive or an assembly reference?)
and
CS0616 'ServiceProvider' is not an attribute class
and more indicating the Jab package contents cannot be resolved.

Jab

I also have the preview of VS2022 installed. There it works perfectly. My colleagues machine uses the same setup with VS2019 and the same code. For him the solution builds without an error.

I'm surely missing something. Thankful for any hints.

Generic services with constraints causes a non-compilable code

Code for reproduction:

        internal interface IService<T>{} where T : IAnotherService
        class ServiceImplementation<T> : IService<T> where T : IAnotherService {  }
        internal interface IAnotherService{} 
        interface IService1 {}
        class ServiceImplementation : IService1 { }
        class ServiceImplementation2 : IAnotherService { }

        [ServiceProvider]
        [Singleton(typeof(IService<>), typeof(ServiceImplementation<>))]
        [Singleton(typeof(IService1), typeof(ServiceImplementation))]
        [Singleton(typeof(IAnotherService), typeof(ServiceImplementation2 ))]
        partial class Container
        {
        }

Expected result:
The code is generated only for IService<IAnotherService>.

Actual result:
The code is generated for IService<IAnotherService> and for IService<IService1>. Thus the generated code cannot be compiled since the type 'IService1' cannot be used as type parameter 'T': there is no implicit reference conversion from 'IService1' to 'IAnotherService'.

Provider module doesn't support factory singletons

Shouldn't this work? I'm getting:

[JAB0003] Unable to find a member 'GetTheService', referenced by 'Factory' attribute parameter

[ServiceProviderModule]
[Singleton(typeof(IService), Factory = nameof(GetTheService))]
public interface IServiceModule
{
     IService GetTheService() => new Service();
}

[ServiceProvider]
[Import(typeof(IServiceModule))]
public partial class MyServiceProvider : IServiceModule { }

public interface IService { void Test(); }

public class Service : IService
{
    public void Test() { Debug.WriteLine("FOO"); }
}

public class Program
{
    public static void Main()
    {
        var provider = new MyServiceProvider();
    }
}

Use in Web API

Just saw a demo of this on YouTube (shoutout to Nick Chapsas... love his channel!) and it looks very interesting! However, 95% of what I do is in a web API. Do you have plans to support a clean way of using Jab in a web API? I could use the default DI to inject my Jab "service provider" into controllers/classes and request what I need from there, but that's not a clean solution IMO.

Stylecop errors with Jab generated code

Should Attributes.cs be marked with <auto-generated/> prefix comment or may be a file name like .g.cs or similar to indicate it is auto generated from the code analysis perspective? Roslyn logic to find if a file is auto generated rules are here.

I also tried explicitly using an .editorconfig file to mark Attributes.cs as auto generated as per docs here. However, it doesn't work as expected. Where is the file generated in the filesystem? Path mentioned by dotnet build (see repro below) doesn't exist.

[Attributes.cs]
generated_code = true

Repro steps

> dotnet --version
6.0.101
> dotnet new console
> dotnet add package jab
> dotnet add package stylecop.analyzers
> dotnet build

# Build warnings
E:\tmp\trial\Jab\Jab.ContainerGenerator\Attributes.cs(155,24): warning SA1201: A interface should not follow a class [E:\tmp\trial\trial.csproj]
E:\tmp\trial\Jab\Jab.ContainerGenerator\Attributes.cs(24,16): warning SA1201: A constructor should not follow a property [E:\tmp\trial\trial.csproj]
E:\tmp\trial\Jab\Jab.ContainerGenerator\Attributes.cs(41,16): warning SA1201: A constructor should not follow a property [E:\tmp\trial\trial.csproj]

Inject factory parameters

I think it would make sense to be able to use parameters for factory methods. For example:

[ServiceProvider]
[Singleton(typeof(IConfiguration), Factory = nameof(CreateConfiguration))]
[Scoped(typeof(DatabaseContext), Factory = nameof(CreateDatabaseContext))]
internal partial class WebApiServiceProvider
{
    private IConfiguration CreateConfiguration()
    {
        //....
    }

    private DatabaseContext CreateDatabaseContext(IConfiguration configuration)
    {
        var connectionString = configuration.GetConnectionString("DefaultConnection");
        //...
    }
}

Now, of course we can do it this way:

internal partial class WebApiServiceProvider
{
    private IConfiguration CreateConfiguration()
    {
        var dbContext = ((IServiceProvider<DatabaseContext>)this).GetService();
        //....
    }

    private DatabaseContext CreateDatabaseContext()
    {
        var configuration = ((IServiceProvider<IConfiguration>)this).GetService();
        //....
    }
}

The only problem is that with this approach you can overlook some problems e. g. circular dependencies:

internal partial class WebApiServiceProvider
{
    private IConfiguration CreateConfiguration()
    {
        var dbContext = ((IServiceProvider<DatabaseContext>)this).GetService(); //do it just for the sake of example
        //...
    }

    private DatabaseContext CreateDatabaseContext()
    {
        var configuration2 = ((IServiceProvider<IConfiguration>)this).GetService();
        //...
    }
}

While if the Jab could inject parameters he would also be able to check provider for the loops.

P. S. Came across this library while checking for compile-time service providers, looks really great!

How big project can be reasonably build with jab?

Hi,

I have been trying to find out how large project can jab support so I have created this branch: https://github.com/kiminuo/jab/tree/feature/di-boom (100 000 classes)

I'm unable to even build this. :-) So just out of curiousity, do you have any idea how large project can be used with jab? I'm after your gut feeling not that much about some precise numbers.

Thanks!

PS: I think that at this point I have hit .NET compiler limit and not really Jab's limit.

IAsyncDisposable in netstandard2.0 projects

Was trying to use Jab within my own source generator project, which needs to be netstandard2.0, of course. The resulting generated Jab code had references to IAsyncDisposable, which isn't a concept netstandard2.0 knows about. Would be nice to omit that related logic when housed in a netstandard2.0 project.

Can I get additional services inside of a ServiceProviderModule factory method?

I found #106, which adds support for factory methods in a ServiceProviderModule.
That is great! I was very happy to see that, thank you @pakrym!

Is there a way, in that static factory method to get additional services?

Maybe the factory method could add an optional parameter of type IServiceProvider that Jab would pass when it calls this method?

My example would be like this:

    internal class MyService : IMyService
    {
         public MyService(MyServiceB otherService){
                 // ...
         }
    }
    public class MyServiceB { }
    public interface IMyService { }

    [ServiceProviderModule]
    [Singleton<MyServiceB>]
    [Singleton<IMyService>(Factory = nameof(MyServiceFactory))]
    public partial interface IModelInitializer
    {
        public static IMyService MyServiceFactory(){
             // there seems to be no way to get MyServiceB from the container here.
             return new MyService(b);
         }
// Suggestion: 
          public static IMyService MyServiceFactory2(IServiceProvider provider){
             return new MyService(provider.GetRequiredService<MyServiceB>());
         }
    }

Unexpected error during code generation - IndexOutOfRange

Hello, I have upgraded visual studio to 17.2.6 from 17.2.4 - and after the upgrade applications using jab have stopped compiling.

Most of the errors were that code was generated several times:

Severity	Code	Description	Project	File	Line	Suppression State
Error	CS0579	Duplicate 'AttributeUsage' attribute	tweetz.core	C:\Users\User\work\tweetz\src\tweetz.core\Jab\Jab.ContainerGenerator\Attributes.cs	7	Active

But the most interasting one is the one states IndexOutOfRange:

Severity	Code	Description	Project	File	Line	Suppression State
Error	JAB0001	Unexpected error occurred during code generation: System.IndexOutOfRangeException: Index was outside the bounds of the array.	tweetz.core	C:\Users\User\work\tweetz\src\tweetz.core\CSC	1	

I am not sure what debug info I can place here that will be of any help, so please let me know :)

Multiple Projects Support

Is it possible to use it on multiple projects? Create modules in a base project using an interface and import it. or create a base class and inherit it?

I have some projects where I do this to register platform specific Implementations and Jab would be a perfect fit as it tells me when I have not implemented a service on compile time!

If you have some suggestions on how this could be achieved I am more than happy to try and implement it myself I just started learning on source generators ๐Ÿ˜„ so I am still trying to get familiar with it!

Jab fails to source generate files on .NET Framework

With the recent version 0.8.3, Jab fails with the following error on .NET Framework:

Severity	Code	Description	Project	File	Line	Suppression State
Error	JAB0001	Unexpected error occurred during code generation: System.InvalidOperationException: Instance module properties are not supported    at Jab.ContainerGenerator.AppendMemberReference(CodeWriter codeWriter, ISymbol method, MemberLocation memberLocation, String rootReference)    at Jab.ContainerGenerator.<>c__DisplayClass5_1.<GenerateCallSite>b__2(CodeWriter w)    at Jab.CodeWriter.Append(CodeWriterDelegate writerDelegate)    at Jab.CodeWriter.AppendFormatted[T](String format, T argument, Int32 index)    at Jab.CodeWriter.AppendInterpolatedStringHandler.AppendFormatted[T](T value)    at Jab.ContainerGenerator.<>c__DisplayClass0_1.<GenerateCallSiteWithCache>b__4(CodeWriter w, CodeWriterDelegate v)    at Jab.ContainerGenerator.GenerateCallSite(CodeWriter codeWriter, String rootReference, ServiceCallSite serviceCallSite, Action`2 valueCallback)    at Jab.ContainerGenerator.GenerateCallSiteWithCache(CodeWriter codeWriter, String rootReference, ServiceCallSite serviceCallSite, Action`2 valueCallback)    at Jab.ContainerGenerator.Execute(GeneratorContext context)	Microsoft.DotNet.ApiCompat.Task	C:\git\sdk\src\ApiCompat\Microsoft.DotNet.ApiCompat.Task\CSC	1	

Unity support (Microsoft.CodeAnalysis 3.8)

Jab seems like a useful tool for Unity projects, as it provides a DI container that avoids runtime reflection and works on platforms with AOT requirements. However, Roslyn support in Unity is lacking behind and restricted to 3.8.

I was able to get Jab.Roslyn3 working in Unity by replacing

<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.11.0" />

with

<PackageReference Include="Microsoft.CodeAnalysis" Version="3.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="3.9.0" />

This causes some [NU1608] Detected package version outside of dependency constraint warnings, but seems to work correctly at least in basic use cases. Common 3.9.0 is required to use RegisterForPostInitialization API in ContainerGenerator.Roslyn3.cs.

What would be the best approach for supporting Unity projects? I can create a pull request either by modifying the Jab.Roslyn3 dependencies or by adding a new target like Jab.Unity.

Support ability to use existing startup registrations.

In a number of existing application, people already have this.

namespace ABC
{
    public static class ServiceRegistration
    {
        public static void AddXYZ(this IServiceCollection services)
        {
            services.AddScoped<IChatProvider, ChatProvider>();
            services.AddCommunityService<CommunityChatService, CommunityChatThreadModel, CommunityChatMessageModel>();
            services.AddScoped<CommunityChatApplication>()
                .AddScoped<ICommunityApplication>(x => x.GetService<CommunityChatApplication>());
            services.AddServerDataFromPathProvider<CommunityChatThreadModel>();
        }
    }
}

Rather than forcing one to put attribute on tons of classes. I would love the generator to be able to read this existing registration from code and infer registrations from it. This would save a lot of code edits. Of course one may have to decorate the static registration class itself with an attribute so the generator can find it.

If you are open to this idea, I'd love to contribute.

Great software BTW.

Is the latest Jab version 0.0.2-beta.101 really released?

I have tried this:

dotnet add package Jab --version 0.0.2-beta.101

  Determining projects to restore...
  Writing C:\Users\USERNAME\AppData\Local\Temp\tmpB3D3.tmp
info : Adding PackageReference for package 'Jab' into project 'C:\project\project.csproj'.
info : Restoring packages for C:\project\project.csproj...
info :   CACHE https://api.nuget.org/v3-flatcontainer/jab/index.json
error: NU1102: Unable to find package Jab with version (>= 0.0.2-beta.101)
error:   - Found 4 version(s) in nuget.org [ Nearest version: 0.0.2-beta.67 ]
error: Package 'Jab' is incompatible with 'all' frameworks in project 'C:\project\project.csproj'.

but it fails for me.

Thank you!

PS: Interestingly, https://www.nuget.org/packages?q=jab makes it quite hard to find the project (https://www.nuget.org/packages/Jab/0.0.2-beta.67)

Provide a changelog

Provide a changelog to alow user keep up with changes made on each version.
The changelog can be provided or on a changelog file or using the Github releases page.

Support nullable as optional dependency

I'm getting problems with optional dependencies as defined by LoggerFactory and ConsoleLoggerProvider.

public LoggerFactory(IEnumerable<ILoggerProvider> providers, IOptionsMonitor<LoggerFilterOptions> filterOption, IOptions<LoggerFactoryOptions>? options)
public ConsoleLoggerProvider(IOptionsMonitor<ConsoleLoggerOptions> options, IEnumerable<ConsoleFormatter>? formatters)

The source generator generates the following invalid code:

// ...
        private System.Collections.Generic.IEnumerable<Microsoft.Extensions.Logging.Console.ConsoleFormatter>?? _IEnumerable_ConsoleFormatter;
// ...
            if (type == typeof(System.Collections.Generic.IEnumerable<Microsoft.Extensions.Logging.Console.ConsoleFormatter>?)) return this.GetService<System.Collections.Generic.IEnumerable<Microsoft.Extensions.Logging.Console.ConsoleFormatter>?>();
// ...

aspnetcore integration

how does this work with MVC Core dependency injection and all of the dependencies that are pre-registered in Startup.cs?

Jab is not working with InternalsVisibleTo

Hi and thanks for great library!

Generated attribute's file generates bunch of issues with when InternalsVisibleTo is in use.
In certain situations there is no way to build solution and current module system works well only in simple projects.
I think that it would be nice to keep Attributes.cs file in external project e.g. Jab.Abstractions.
Andrew Lock wrote article about this issue (in scope of his StronglyTypedId library).
Please take a look on possible solution, here is link to article:
https://andrewlock.net/creating-a-source-generator-part-7-solving-the-source-generator-marker-attribute-problem-part1/

Here also is discussion about similar issue andrewlock/StronglyTypedId#38

Best regards!

.NET 7.0 Support

Tried using this with .NET 7.0, preview 6. Won't compile due to errors. Here's a sampling of the errors.

error CS0101: The namespace 'Jab' already contains a definition for 'ServiceProviderAttribute'
error CS0101: The namespace 'Jab' already contains a definition for 'ServiceProviderModuleAttribute'
error CS0101: The namespace 'Jab' already contains a definition for 'ImportAttribute'
error CS0101: The namespace 'Jab' already contains a definition for 'SingletonAttribute'
error CS0101: The namespace 'Jab' already contains a definition for 'TransientAttribute'
error CS0101: The namespace 'Jab' already contains a definition for 'ScopedAttribute'
error CS0101: The namespace 'Jab' already contains a definition for 'IServiceProvider'
error CS0101: The namespace 'Jab' already contains a definition for 'JabHelpers'
error CS0579: Duplicate 'AttributeUsage' attribute
error CS0579: Duplicate 'AttributeUsage' attribute
error CS0579: Duplicate 'AttributeUsage' attribute
error CS0111: Type 'ImportAttribute' already defines a member called 'ImportAttribute' with the same parameter types
error CS0579: Duplicate 'AttributeUsage' attribute
error CS0111: Type 'SingletonAttribute' already defines a member called 'SingletonAttribute' with the same parameter types
error CS0111: Type 'SingletonAttribute' already defines a member called 'SingletonAttribute' with the same parameter types
error CS0579: Duplicate 'AttributeUsage' attribute
error CS0111: Type 'TransientAttribute' already defines a member called 'TransientAttribute' with the same parameter types
error CS0111: Type 'TransientAttribute' already defines a member called 'TransientAttribute' with the same parameter types
error CS0579: Duplicate 'AttributeUsage' attribute
error CS0111: Type 'ScopedAttribute' already defines a member called 'ScopedAttribute' with the same parameter types
error CS0111: Type 'ScopedAttribute' already defines a member called 'ScopedAttribute' with the same parameter types
error CS0111: Type 'JabHelpers' already defines a member called 'CreateServiceNotFoundException' with the same parameter types
error CS0111: Type 'IServiceProvider<T>' already defines a member called 'GetService' with the same parameter types

version 0.8.3

Support pointing a Factory to a Func

First of all, this library is pure gold. I love it!

When the service provider itself can't implement a factory, you want to pass that in via a Func. Unfortunately it looks like jab currently doesn't support that:

Severity	Code	Description	Project	File	Line	Suppression State
Error	CS0029	Cannot implicitly convert type 'System.Func<Microsoft.DotNet.ApiCompatibility.Logging.CompatibilityLoggerBase>' to 'Microsoft.DotNet.ApiCompatibility.Logging.CompatibilityLoggerBase'	Microsoft.DotNet.Compatibility.Console	C:\git\sdk\src\Compatibility\Microsoft.DotNet.Compatibility.Console\Jab\Jab.ContainerGenerator\CompatibilityServiceProvider.Generated.cs	35	Active
    [ServiceProvider]
    [Singleton(typeof(CompatibilityLoggerBase), Factory = nameof(LogFactory))]
    internal partial class CompatibilityServiceProvider
    {
        public Func<CompatibilityLoggerBase> LogFactory { get; }

        public CompatibilityServiceProvider(Func<CompatibilityLoggerBase> logFactory)
        {
            LogFactory = logFactory;
        }
    }

As far as I can tell, the relevant code that constructs the method call is

var member = registration.InstanceMember ?? registration.FactoryMember;
switch (member)
{
case IMethodSymbol instanceMethod:
callSite = CreateMethodCallSite(
registration.ServiceType,
registration.ImplementationType,
registration.Lifetime,
registration.Location,
registration.IsScopeMember, instanceMethod, reverseIndex, context);
break;
case {} factoryMember:
callSite = CreateMemberCallSite(
registration,
factoryMember,
registration.IsScopeMember,
reverseIndex);
break;
and would need to be updated to treat Func similar to a IMethodSymbol.

Removing unnecessary code from output assembly

I just tried to migrate one of my projects from Injectio to Jab and one issue I came across is that the Marker attributes required for annotating the service provider are also contained in my output assembly when building. Is it possible to remove the attributes from the output assembly?

This is an issue to me, because it messes with the code coverage, and also because it is just not necessary to have these markers referenced in the assembly after building.

From what I see it should be possible to use conditionals or msbuild variables to hide these markers. I'm not really familiar with SG yet, but thats what I understood from Andrew Lock's blogpost.

Here is an image on how this looks on my code coverage report1.

image

Footnotes

  1. Dont make fun of my coverage, I'm working on it, it's just a rapid prototype. โ†ฉ

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.