reinforced / Reinforced.Typings

Converts C# classes to TypeScript interfaces (and many more) within project build. 0-dependency, minimal, gluten-free
MIT License
498 stars 80 forks source link

Dependency Injection support or getting all implementations of an interface. #121

Open johnrom opened 5 years ago

johnrom commented 5 years ago

This question is related to my attempts here: #120 re: dynamically generating API view models and discriminating them. The goal is to build a project, get all registered implementations of a base view model, and create a discriminator between them. This will allow the creation of a new API view model to automatically trigger build issues in typescript, since those new view models will not have been added to the discriminator function on the front end.

I understand that the current TS build happens during build and not during Startup. The hope is to create dependency injected types by hooking into the build process via a middleware after startup.

Even without the dependency injection, if I were to get all of the implementations via Reflection, is there a way to create a discriminated union type?

Example dependency injection API below. I'm using the same IReturnValue example from the linked issue #120 .

// Startup.cs
public void ConfigureServices(IServiceCollection services) {
    services
        .AddReinforcedTypings()
        .UseConfiguration<TsConfiguration>();
}

// TsConfiguration.cs
public class TsConfiguration : IReinforcedTypingsConfiguration
{
    private readonly IEnumerable<IReturnValue> _returnValues;

    public TsConfiguration(
        IEnumerable<IReturnValue> returnValues
    ) {
        _returnValues= returnValues;
    }

    public static void Configure(ConfigurationBuilder builder)
    {
        // the usual stuff
        builder.Global(config => {
            config.UseModules();
            config.CamelCaseForProperties();
            config.AutoOptionalProperties();
        });

        builder.ExportAsDiscriminant("ApiReturnValueList", _returnValues);
    }
}
// api-models.ts
export type ApiModelList = 
    MyFirstReturnValue |
    MySecondReturnValue;

// my-models.ts
export type ApiModel = {
    returnValue: ApiModelList;
}
pavel-b-novikov commented 5 years ago

So in which moment you'd like output files to be generated? On build? On startup?

pavel-b-novikov commented 5 years ago

No response

johnrom commented 5 years ago

FYI I don't have the answer, but I still think there should be a way to access Dependendency Injection in order to build TypeScript types. Startup doesn't make sense for Publishing. Technically it is a Build process. So there should be a way in the build process to access the same (or similar) dependency pipeline. I have some thoughts, but I am unfamiliar with the codebase so not sure how compatible this system would be with the build process.

The Reinforced.Typings process in .Net Standard (not sure how this falls back to .Net Framework) should create its own Console Host with a configurable Startup. For which startup to use, there are a few options:

  1. Using the same startup as the main project, calling the same ConfigureServices from Startup, and once that is complete, run the TypeScript build. This is probably the easiest to configure, but would probably result in a lot of pointless calls to services we don't care about and maybe security concerns as users add DB connections and other sensitive configurations here. Additionally, if there is another round of startups within a CMS or something, this might not actually add all the services (looking at Orchard Core).
  2. Using the same startup but not calling Configure() or ConfigureServices(), but calling ConfigureReinforcedTypings(). This would allow someone to create an extension like services.AddMyApiServices(), which they would call from both ConfigureServices and ConfigureTypings on the serviceCollection. I don't think this is going to be compatible with the .Net Generic Host since I believe Configure and ConfigureServices are required entrypoints.
  3. Using a different Startup kind of like the current way ConfigureReinforcedTypings functions, but instead use the standard ConfigureServices and Configure, one to add the common services (accessible via the same extension defined in userland in option 2) and one to build the configuration.

I work more in the runtime than build processes, so even though I might not have an answer, I don't think this feature request should be closed.

pavel-b-novikov commented 5 years ago

Well, it is possible to run RT without console. That is how. This code can be wrapped into separate console host, process, thread or whatever you'd like. So I would appreciate pull request enchancing RT functionality by implementing your idea, because still cannot get full understanding how it must work.

johnrom commented 5 years ago

That's fine if someone wants to circumvent the normal build process, but I'm recommending updating the normal build process. Right now, the Console looks like this:

// Bootstrapper.cs
public static class Reinforced.Typings.Cli {
    public static Main() {
        // Do the work here
        new TSExporter().export();
    }
}

I recommend that if NETCORE (or if .Net Framework has support for Microsoft.Extensions.Hosting, cool) and Configuration includes <UseStartup>MyProject.TsConfiguration.Startup</UseStartup> (or something) then (I'm just winging this)

using Microsoft.Extension.Hosting; // Generic Console host

public static class Reinforced.Typings.Cli {
    public static Main() {
        // Get value of UseStartup()
        var userlandStartupDefinition = GetConfiguration("UseStartup");
        var userlandStartup = GetType(userlandStartupDefinition); // or whatever
        IReinforcedTypingsStartup reinforcedTypingsStartup;

        if (userlandStartup is IReinforcedTypingsStartup rts) {
            reinforcedTypingsStartup = rts;
        }

        var host = new HostBuilder()
            .ConfigureServices(services => {
                if (reinforcedTypingsStartup != null) userlandStartup.ConfigureServices(services);

                services.AddHostedService<ReinforcedTypingsEntrypoint>();
            })
            .UseConsoleLifetime()
            .Build();

        await host.RunAsync();
    }
}

// ReinforcedTypingsEntrypoint.ts
public class ReinforcedTypingsEntrypoint: IHostedService, IDisposable {
    private readonly IServiceProvider _serviceProvider ;

    public ReinforcedTypingsEntrypoint(IServiceProvider serviceProvider) {
        _serviceProvider = serviceProvider;
    }

    public async Task StartAsync(CancellationToken cancellationToken) {
        if (!cancellationToken.IsCancellationRequested) {
            // enter scope, like you would in an HTTP request so all DI can access scoped services
            using (var scope = _serviceProvider.CreateScope())
            {
                var exporter = scope.ServiceProvider.GetRequiredService<TSExporter>();

                await exporter.Export(cancellationToken);
            }
        }
    }
}
pavel-b-novikov commented 5 years ago

That is beautiful, but please provide PR

johnrom commented 5 years ago

I'll try to circle back to it, but if you keep this issue closed then it will be buried in the "Closed Issues" section. Can you re-open and label it something you can recognize as awaiting requestor?

lemoinem commented 5 years ago

We are interested in generating discriminated unions as well.

@johnrom Even without the complexity of changing the build process or removing the need to repeat the constant value (as mentioned in #120), don't think we could have a quick-win solution that could simply generate an explicitly specified discriminated union?

For example [TsUnion(typeof(ImplementationA), typeof(ImplementationB))]?

@pavel-b-novikov Would you be interested in a PR for this?

pavel-b-novikov commented 5 years ago

Tbh, I do not fully understand what TS output you expect with TsUnion

johnrom commented 5 years ago

@lemoinem that would remove the magic of singly registering the dependency via dependency injection. You now have to maintain two lists of the same components, one via dependency injection, and one via TsUnion.

If I have something like IFormDataApiModel, 25 forms on the website, register each of them via dependency injection, and intend to use them as typescript ajax form submission models, the point is that maintaining a list of IFormDataApiModels on the TypeScript side is going to be a bad experience over time. Maintaining that second list in C# wouldn't be any better in my opinion.

Edit for clarity: I'm not trying to argue against implementing something like what you described, just that it's not a solution for this issue, and it might be better to open a new one.

lemoinem commented 5 years ago

@johnrom To be clear, what I'm offering is basically something outside of the scope of using dependency injection. I'm looking something that would achieve a similar goal (i.e. a similar TS output), but would be easier to implement within the current code base.

PS: I though the issue focused on discussing solutions for discriminated unions. Sorry if I hijacked an unrelated issue, I'll move the discussion over to the other issue. I had a hard time understanding the goal of changing the build process and using DI. I think it's much clearer now. Thanks!

johnrom commented 5 years ago

@lemoinem no worries! I created feature requests for things which will support the usage of discriminated unions, but I never created a request to implement them in the API, so I can understand your confusion. In my case, I'd be creating DU from Fluent Configuration instead of as an attribute, so if you create an issue specifically for DU, we can discuss there.