Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support DI registration methods #202

Closed
latonz opened this issue Dec 12, 2022 · 5 comments
Closed

Support DI registration methods #202

latonz opened this issue Dec 12, 2022 · 5 comments
Labels
enhancement New feature or request

Comments

@latonz
Copy link
Contributor

latonz commented Dec 12, 2022

Via an option on the MapperAttribute, extension methods to the IServiceCollection should be generated to add a single mapper by its name or all mappers at once to the service collection. By default these should be added as singletons, but an overload with a service lifetime should be available.

Example of how the generated code could look like:

namespace Microsoft.Extensions.DependencyInjection;

public static class MapperlyExtensions
{
    public static IServiceCollection AddMyMapper(this IServiceCollection services)
        => services.AddMyMapper(ServiceLifetime.Singleton);

    public static IServiceCollection AddMyMapper(this IServiceCollection services, ServiceLifetime lifetime)
        => services.Add<MyMapper>(lifetime);

    public static IServiceCollection AddMappers()
        => services.AddMyMapper();

    public static IServiceCollection AddMappers(ServiceLifetime lifetime)
        => services.AddMyMapper(lifetime);
}

tbd: currently, Mapperly only generates implementations for user defined partial methods and Mapperly-internal private methods. This would generate public accessible code, without the user defining it. Does that have any implications on usability?

@latonz latonz added the enhancement New feature or request label Dec 12, 2022
@mshgh
Copy link

mshgh commented Dec 12, 2022

I can see you want to go with source code generation consistently for everything, makes sense. In this case how would you feel about following approach. This would be analogical to the mappers, and would address your concern about generating public code w/o user awareness.

// mapperly abstractions - probably better name
public class AddMappersAttribute : Attribute
{
  public AddMappersAttribute(ServiceLifetime lifetime, params Type[] mappers)...
}

// user code
public static partial class MyComponentExtensions
{
  public static IServiceCollection AddMyComponent(this IServiceCollection services) => services
    .AddTransient<IFoo, Foo>()
    .AddTransientMappersNameDoesntMatter()
    .AddSigneletonMappers();

  [AddMappers(ServiceLifetime.Transient, typeof(CarMapper), typeof(CustomerMapper))]
  private static partial IServiceCollection AddTransientMappersNameDoesntMatter(this IServiceCollection services);

  [AddMappers(ServiceLifetime.Singleton, typeof(ShopMapper), typeof(InvoiceMapper))]
  private static partial IServiceCollection AddSingletonMappers(this IServiceCollection services);
}

this would generate code similar to this one

public static partial class MyComponentExtensions
{
  private static partial IServiceCollection AddTransientMappersNameDoesntMatter(this IServiceCollection services) => services
    .AddTransient<CarMapper>()
    .AddTransient<CustomerMapper>();

  private static partial IServiceCollection AddSingletonMappers(this IServiceCollection services) => services
    .AddSingleton<ShopMapper>()
    .AddSingleton<InvoiceMapper>();
}

@zsolt3991
Copy link

It would be nice to have the possibility of registering mappers with the DI container extended with some form of abstractions to allow taking dependencies on some known contract instead of the individual mapper implementations. It should be possible to source generate interface implementations, the MVVM toolkit already does something like this.

It could look something like this:

  • there is an interface in the included abstractions having source and destination type parameters and a single mapper "Map" method.
  • user defined partial methods would define which Interfaces the mapper would implement. This could be marked by an attribute on the method to expose it through an interface (not sure if this could already also enforce the naming of the method to match)
  • registrations are done against all the implemented interfaces, respecting the lifetime chosen

@latonz
Copy link
Contributor Author

latonz commented Dec 12, 2022

On a second thought, I think Mapperly should keep the principle of only generating implementations for user defined partial methods and Mapperly-Private methods. This keeps the features discoverable and less magical to the user. Closing this for now.

@latonz latonz closed this as completed Dec 12, 2022
@Khaos66
Copy link

Khaos66 commented Sep 18, 2024

Oh man! I just found this cool library. Thank's for building it <3
Sadly DI support was rejected too soon, I guess.

I'm comming from AutoMapper (just as everybody, I guess).
There I could create some generic methods mapping just anything really.
A very simplified example:

class EndpointBase<TRequest,TCommand>(IMapper mapper)
{
   public TCommand CreateCommand(TRequest req) =>
       mapper.Map<TRequest, TCommand>(req)
}

I think this could be achieved by this lib, too.
Let's have an interface for mappers:

public interface IMapper { }
public interface IMapper<TSource, TTarget> : IMapper 
{
   TTarget Map<TSource, TTarget>(TSource source);
}

This interface could be added by the source gen to each mapper class. The Map method could just call the user defined MapCarToCarDto function ;)

For DI to work, there should be a AddMappers extension method.
Maybe the user can decide on which extension class he would like the method to be generated.

static partial class ServiceExtensions
{
   [MapperExtensionMethod(ServiceScope = Scoped)]
   public static partial IServiceCollection AddMyMappers(this IServiceCollection services);
}

The source gen would then create a method like this containing all the mappers:

public static partial IServiceCollection AddMyMappers(this IServiceCollection services)
{
   services.AddScoped<IMapper<Car, CarDto>, CarMapper>();
   services.AddScoped<IMapper<Cat, CatDto>, OtherMapper>();
   services.AddScoped<IMapper<Dog, DogDto>, OtherMapper>();
   return services;
}

This would enable users of the lib to register mappers with DI and consume them via the IMapper<TSource, TTarget> interface.

I know there are some edge cases, where other method signatures are supported for the mapping methods.
Like the one with an existing target object.
There could be more interfaces that encapulate them as well, like ITargetMapper or the IMapper interface defines another method like Map(TSource source, TTarget target) and the source gen generates a method which throws an NotImplementedException for all missing mapping methods.

@latonz What do you think? Should I prepare a PR?

@latonz
Copy link
Contributor Author

latonz commented Sep 18, 2024

Thanks for your feedback and willingness to prepare a PR! 😊 New contributors are always welcome.

I'm not really sure if I understand your comment correctly. If not, could you provide an example of what the generated code would look like for the feature you are requesting?

As far as I understand your comment there are two requests:

  1. an alternative configuration API surface via method invocations instead of attributes and method signatures
  2. generating dependency injection service registration code

My two cents:

  1. Source generators have a hard time using regular c# statements as configuration API (the source generator only has access to the semantic model and to the syntax, executing code at compile time is currently not in the scope of source generators...). Furthermore source generators need to know exactly what to code to generate at compile time. This means Mapperly needs to know all types at compile time, which can be mapped by the application at runtime. Mapperly achieves this by a combination of attributes and method signatures which are both well accessible in the semantic model. An approach with method calls to configure the mappings would either need isolation and compilation of the given calls or a complex source analysis. Both would probably result in an explosion of the complexity of Mapperly‘s source code. If you have a good idea on how to implement such a configuration API in a .NET source generator I would be very excited to hear about it 😊 See also Alternative syntax for mapping properties? #1365 (comment).
  2. The complexity of the service collection registration for a Mapperly mapper should be quite trivial and no different than for any other class. It should be as simple as services.AddSingleton<MyMapper>(). If one really wants to abstract this away with a source generator it should even work with other source generators (e.g. AutoRegisterInject, I never tried it though).

Before you start working on an implementation, we should discuss this matter further until we have a concrete plan of what should be implemented and how.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants