Skip to content

Commit fd06570

Browse files
authored
Merge pull request #4603 from LuckyPennySoftware/4602-enable-di-for-constructusing
Adding support for DI-enabled destination factories.
2 parents 603df79 + 8e5248b commit fd06570

12 files changed

Lines changed: 325 additions & 51 deletions

docs/source/Construction.md

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,77 @@ You can configure which constructors are considered for the destination object:
5353
// use only public constructors
5454
var configuration = new MapperConfiguration(cfg => cfg.ShouldUseConstructor = constructor => constructor.IsPublic, loggerFactory);
5555
```
56-
When mapping to records, consider using only public constructors.
56+
When mapping to records, consider using only public constructors.
57+
58+
## Class-Based Destination Factories
59+
60+
Instead of automatic constructor matching or inline `ConstructUsing` lambdas, you can implement custom constructor logic as a class. This enables reuse across multiple mappings and supports dependency injection.
61+
62+
This is different than `ConvertUsing` which replaces the entire mapping operation.
63+
64+
### Interface
65+
66+
`IDestinationFactory<TSource, TDestination>` is used to implement custom object construction:
67+
68+
```csharp
69+
public interface IDestinationFactory<in TSource, out TDestination>
70+
{
71+
TDestination Construct(TSource source, ResolutionContext context);
72+
}
73+
```
74+
75+
### Usage
76+
77+
```csharp
78+
public class CustomConstructor : IDestinationFactory<Source, Destination>
79+
{
80+
public Destination Construct(Source source, ResolutionContext context)
81+
{
82+
// Custom instantiation logic
83+
return new Destination { InitialValue = source.Value * 2 };
84+
}
85+
}
86+
87+
cfg.CreateMap<Source, Destination>()
88+
.ConstructUsing<CustomConstructor>();
89+
```
90+
91+
### Dependency Injection
92+
93+
Destination factories are resolved from the DI container, enabling constructor injection of services:
94+
95+
```csharp
96+
public class DIAwareConstructor : IDestinationFactory<Source, Destination>
97+
{
98+
private readonly IMyService _service;
99+
100+
public DIAwareConstructor(IMyService service)
101+
{
102+
_service = service;
103+
}
104+
105+
public Destination Construct(Source source, ResolutionContext context)
106+
{
107+
return new Destination
108+
{
109+
InitialValue = _service.CalculateValue(source.Value)
110+
};
111+
}
112+
}
113+
114+
// Registration
115+
services.AddScoped<IMyService, MyService>();
116+
services.AddAutoMapper(cfg =>
117+
{
118+
cfg.CreateMap<Source, Destination>()
119+
.ConstructUsing<DIAwareConstructor>();
120+
}, typeof(IMyService).Assembly);
121+
```
122+
123+
For runtime type resolution, use the non-generic overload:
124+
125+
```csharp
126+
cfg.CreateMap(typeof(Source), typeof(Destination))
127+
.ConstructUsing(typeof(CustomConstructor));
128+
```
129+

docs/source/Dependency-injection.md

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -55,55 +55,11 @@ When using `AddAutoMapper`, AutoMapper will automatically register implementatio
5555
- `IMemberValueResolver<TSource, TDestination, TSourceMember, TDestMember>`
5656
- `ITypeConverter<TSource, TDestination>`
5757
- `IValueConverter<TSourceMember, TDestinationMember>`
58+
- `IDestinationFactory<TSource, TDestination>`
5859
- `ICondition<TSource, TDestination, TMember>`
5960
- `IPreCondition<TSource, TDestination>`
6061
- `IMappingAction<TSource, TDestination>`
6162

62-
This allows you to use class-based conditions with dependency injection:
63-
64-
```c#
65-
public class MyCondition : ICondition<Source, Destination, int>
66-
{
67-
private readonly IMyService _myService;
68-
69-
public MyCondition(IMyService myService)
70-
{
71-
_myService = myService;
72-
}
73-
74-
public bool Evaluate(Source source, Destination destination, int sourceMember,
75-
int destMember, ResolutionContext context)
76-
{
77-
return _myService.ShouldMap(sourceMember);
78-
}
79-
}
80-
81-
public class ConditionProfile : Profile
82-
{
83-
public ConditionProfile()
84-
{
85-
CreateMap<Source, Destination>()
86-
.ForMember(d => d.Value, o =>
87-
{
88-
o.Condition<MyCondition>();
89-
o.MapFrom(s => s.Value);
90-
});
91-
}
92-
}
93-
94-
// In Startup.cs / Program.cs:
95-
services.AddTransient<IMyService, MyService>();
96-
services.AddAutoMapper(cfg => { }, typeof(ConditionProfile).Assembly);
97-
```
98-
99-
Or dynamic service location, to be used in the case of instance-based containers (including child/nested containers):
100-
101-
```c#
102-
var mapper = new Mapper(configuration, childContainer.GetInstance);
103-
104-
var dest = mapper.Map<Source, Destination>(new Source { Value = 15 });
105-
```
106-
10763
## Queryable Extensions
10864

10965
Starting with 8.0 you can use `IMapper.ProjectTo`. For older versions you need to pass the configuration to the extension method ``` IQueryable.ProjectTo<T>(IConfigurationProvider) ```.

src/AutoMapper.DI.Tests/AppDomainResolutionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public void ShouldResolveConfiguration()
3030
[Fact]
3131
public void ShouldConfigureProfiles()
3232
{
33-
_provider.GetService<IConfigurationProvider>().Internal().GetAllTypeMaps().Count.ShouldBe(5);
33+
_provider.GetService<IConfigurationProvider>().Internal().GetAllTypeMaps().Count.ShouldBe(6);
3434
}
3535

3636
[Fact]

src/AutoMapper.DI.Tests/AssemblyResolutionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public void ShouldResolveConfiguration()
3737
[Fact]
3838
public void ShouldConfigureProfiles()
3939
{
40-
_provider.GetService<IConfigurationProvider>().Internal().GetAllTypeMaps().Count.ShouldBe(5);
40+
_provider.GetService<IConfigurationProvider>().Internal().GetAllTypeMaps().Count.ShouldBe(6);
4141
}
4242

4343
[Fact]

src/AutoMapper.DI.Tests/DependencyTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,42 @@ public void ShouldSkipMappingWithDependency_WhenConditionFails()
7777
dest.Value.ShouldBe(0);
7878
}
7979
}
80+
81+
public class DestinationFactoryDependencyTests
82+
{
83+
private readonly IServiceProvider _provider;
84+
85+
public DestinationFactoryDependencyTests()
86+
{
87+
IServiceCollection services = new ServiceCollection();
88+
services.AddTransient<ISomeService>(sp => new FooService(5));
89+
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
90+
services.AddAutoMapper(_ => { }, typeof(FactorySource));
91+
_provider = services.BuildServiceProvider();
92+
93+
_provider.GetService<IConfigurationProvider>().AssertConfigurationIsValid();
94+
}
95+
96+
[Fact]
97+
public void ShouldConstructWithDependency()
98+
{
99+
var mapper = _provider.GetService<IMapper>();
100+
var dest = mapper.Map<FactorySource, FactoryDest>(new FactorySource { Value = 10 });
101+
102+
// FooService.Modify(10) = 10 + 5 = 15
103+
dest.InitialValue.ShouldBe(15);
104+
dest.Value.ShouldBe(10);
105+
}
106+
107+
[Fact]
108+
public void ShouldConstructWithDependency_DifferentValue()
109+
{
110+
var mapper = _provider.GetService<IMapper>();
111+
var dest = mapper.Map<FactorySource, FactoryDest>(new FactorySource { Value = 20 });
112+
113+
// FooService.Modify(20) = 20 + 5 = 25
114+
dest.InitialValue.ShouldBe(25);
115+
dest.Value.ShouldBe(20);
116+
}
117+
}
80118
}

src/AutoMapper.DI.Tests/Profiles.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,35 @@ public ConditionProfile()
183183
});
184184
}
185185
}
186+
187+
public class FactorySource
188+
{
189+
public int Value { get; set; }
190+
}
191+
192+
public class FactoryDest
193+
{
194+
public int Value { get; set; }
195+
public int InitialValue { get; set; }
196+
}
197+
198+
public class DependencyFactory : IDestinationFactory<FactorySource, FactoryDest>
199+
{
200+
private readonly ISomeService _service;
201+
202+
public DependencyFactory(ISomeService service) => _service = service;
203+
204+
public FactoryDest Construct(FactorySource source, ResolutionContext context)
205+
=> new FactoryDest { InitialValue = _service.Modify(source.Value) };
206+
}
207+
208+
internal class FactoryProfile : Profile
209+
{
210+
public FactoryProfile()
211+
{
212+
CreateMap<FactorySource, FactoryDest>()
213+
.ConstructUsing<DependencyFactory>()
214+
.ForMember(dest => dest.InitialValue, opt => opt.Ignore());
215+
}
216+
}
186217
}

src/AutoMapper.DI.Tests/TypeResolutionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public void ShouldResolveConfiguration()
3333
[Fact]
3434
public void ShouldConfigureProfiles()
3535
{
36-
_provider.GetService<IConfigurationProvider>().Internal().GetAllTypeMaps().Count.ShouldBe(5);
36+
_provider.GetService<IConfigurationProvider>().Internal().GetAllTypeMaps().Count.ShouldBe(6);
3737
}
3838

3939
[Fact]

src/AutoMapper/Configuration/IMappingExpressionBase.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,20 @@ public interface IMappingExpressionBase<TSource, TDestination, out TMappingExpre
157157
/// <returns>Itself</returns>
158158
TMappingExpression ConstructUsing(Func<TSource, ResolutionContext, TDestination> ctor);
159159
/// <summary>
160+
/// Supply a custom object constructor type for instantiating the destination type with dependency injection support
161+
/// </summary>
162+
/// <remarks>Not used for LINQ projection (ProjectTo).</remarks>
163+
/// <typeparam name="TConstructor">Constructor type implementing IDestinationFactory&lt;TSource, TDestination&gt;</typeparam>
164+
/// <returns>Itself</returns>
165+
TMappingExpression ConstructUsing<TConstructor>() where TConstructor : IDestinationFactory<TSource, TDestination>;
166+
/// <summary>
167+
/// Supply a custom object constructor type for instantiating the destination type with dependency injection support.
168+
/// Used when the constructor type is not known at compile-time.
169+
/// </summary>
170+
/// <remarks>Not used for LINQ projection (ProjectTo).</remarks>
171+
/// <param name="objectConstructorType">Constructor type implementing IDestinationFactory</param>
172+
void ConstructUsing(Type objectConstructorType);
173+
/// <summary>
160174
/// Override the destination type mapping for looking up configuration and instantiation
161175
/// </summary>
162176
/// <param name="typeOverride"></param>
@@ -197,6 +211,22 @@ public interface IMappingExpressionBase<TSource, TDestination, out TMappingExpre
197211
/// <typeparam name="TTypeConverter">Type converter type</typeparam>
198212
void ConvertUsing<TTypeConverter>() where TTypeConverter : ITypeConverter<TSource, TDestination>;
199213
}
214+
215+
/// <summary>
216+
/// Custom destination factory for instantiating destination objects with dependency injection support.
217+
/// </summary>
218+
/// <typeparam name="TSource">Source type</typeparam>
219+
/// <typeparam name="TDestination">Destination type</typeparam>
220+
public interface IDestinationFactory<in TSource, out TDestination>
221+
{
222+
/// <summary>
223+
/// Construct the destination object from the source object
224+
/// </summary>
225+
/// <param name="source">Source object</param>
226+
/// <param name="context">Resolution context</param>
227+
/// <returns>New destination instance</returns>
228+
TDestination Construct(TSource source, ResolutionContext context);
229+
}
200230
/// <summary>
201231
/// Custom mapping action
202232
/// </summary>

src/AutoMapper/Configuration/TypeMapConfiguration.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,16 @@ public TMappingExpression ConstructUsing(Func<TSource, ResolutionContext, TDesti
325325
Expression<Func<TSource, ResolutionContext, TDestination>> expr = (src, ctxt) => ctor(src, ctxt);
326326
return ConstructUsingCore(expr);
327327
}
328+
public TMappingExpression ConstructUsing<TConstructor>() where TConstructor : IDestinationFactory<TSource, TDestination>
329+
{
330+
TypeMapActions.Add(tm => tm.ConstructUsingObjectConstructor(typeof(TConstructor)));
331+
return this as TMappingExpression;
332+
}
333+
334+
public void ConstructUsing(Type objectConstructorType)
335+
{
336+
TypeMapActions.Add(tm => tm.ConstructUsingObjectConstructor(objectConstructorType));
337+
}
328338
public void ConvertUsing(Type typeConverterType)
329339
{
330340
HasTypeConverter = true;

src/AutoMapper/ServiceCollectionExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace Microsoft.Extensions.DependencyInjection;
1515
/// Extensions to scan for AutoMapper classes and register the configuration, mapping, and extensions with the service collection:
1616
/// <list type="bullet">
1717
/// <item> Finds <see cref="Profile"/> classes and initializes a new <see cref="MapperConfiguration" />,</item>
18-
/// <item> Scans for <see cref="ITypeConverter{TSource,TDestination}"/>, <see cref="IValueResolver{TSource,TDestination,TDestMember}"/>, <see cref="IMemberValueResolver{TSource,TDestination,TSourceMember,TDestMember}" />, <see cref="ICondition{TSource,TDestination,TMember}"/>, <see cref="IPreCondition{TSource,TDestination}"/>, <see cref="IValueConverter{TSourceMember,TDestinationMember}"/> and <see cref="IMappingAction{TSource,TDestination}"/> implementations and registers them as <see cref="ServiceLifetime.Transient"/>, </item>
18+
/// <item> Scans for <see cref="ITypeConverter{TSource,TDestination}"/>, <see cref="IValueResolver{TSource,TDestination,TDestMember}"/>, <see cref="IMemberValueResolver{TSource,TDestination,TSourceMember,TDestMember}" />, <see cref="IDestinationFactory{TSource,TDestination}"/>, <see cref="ICondition{TSource,TDestination,TMember}"/>, <see cref="IPreCondition{TSource,TDestination}"/>, <see cref="IValueConverter{TSourceMember,TDestinationMember}"/> and <see cref="IMappingAction{TSource,TDestination}"/> implementations and registers them as <see cref="ServiceLifetime.Transient"/>, </item>
1919
/// <item> Registers <see cref="IConfigurationProvider"/> as <see cref="ServiceLifetime.Singleton"/>, and</item>
2020
/// <item> Registers <see cref="IMapper"/> as a configurable <see cref="ServiceLifetime"/> (default is <see cref="ServiceLifetime.Transient"/>)</item>
2121
/// </list>
@@ -24,7 +24,7 @@ namespace Microsoft.Extensions.DependencyInjection;
2424
/// </summary>
2525
public static class ServiceCollectionExtensions
2626
{
27-
static readonly Type[] AmTypes = [typeof(IValueResolver<,,>), typeof(IMemberValueResolver<,,,>), typeof(ITypeConverter<,>), typeof(IValueConverter<,>), typeof(ICondition<,,>), typeof(IPreCondition<,>), typeof(IMappingAction<,>)];
27+
static readonly Type[] AmTypes = [typeof(IValueResolver<,,>), typeof(IMemberValueResolver<,,,>), typeof(ITypeConverter<,>), typeof(IValueConverter<,>), typeof(IDestinationFactory<,>), typeof(ICondition<,,>), typeof(IPreCondition<,>), typeof(IMappingAction<,>)];
2828
public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action<IMapperConfigurationExpression> configAction)
2929
=> AddAutoMapperClasses(services, (sp, cfg) => configAction?.Invoke(cfg), null);
3030

0 commit comments

Comments
 (0)