我正在使用 AutoMapper,我需要在我的一个 Profile 类中注入一项服务。AutoMapper Profile 类并非设计用于支持依赖注入,在这种情况下,记录的模式是……
我正在使用 AutoMapper ,我需要在我的一个 Profile
类中注入一项服务。
AutoMapper Profile
类不是为了支持依赖注入而设计的,在这种情况下应该使用记录的模式 IMappingAction
( 有关更多详细信息 此处
这些是我需要使用 AutoMapper 映射的类:
public sealed class Student
{
public int Id { get; set; }
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public DateOnly BirthDate { get; set; }
}
public sealed class StudentDto
{
public string FullName { get; set; } = string.Empty;
public DateOnly BirthDate { get; set; }
public string SerialNumber { get; set; } = string.Empty;
}
这是 Profile
和 Student
之间的映射的类 StudentDto
:
public sealed class StudentProfile : Profile
{
public StudentProfile()
{
CreateMap<Student, StudentDto>()
.ForMember(
studentDto => studentDto.FullName,
options => options.MapFrom(student => $"{student.FirstName} {student.LastName}")
)
.ForMember(
studentDto => studentDto.SerialNumber,
options => options.Ignore()
)
.AfterMap<SetSerialNumberAction>();
}
}
该类 Profile
引用以下映射操作:
public sealed class SetSerialNumberAction : IMappingAction<Student, StudentDto>
{
private readonly ISerialNumberProvider _serialNumberProvider;
public SetSerialNumberAction(ISerialNumberProvider serialNumberProvider)
{
_serialNumberProvider = serialNumberProvider ?? throw new ArgumentNullException(nameof(serialNumberProvider));
}
public void Process(Student source, StudentDto destination, ResolutionContext context)
{
ArgumentNullException.ThrowIfNull(destination);
destination.SerialNumber = _serialNumberProvider.GetSerialNumber();
}
}
最后,这是在类中注入的服务 SetSerialNumberAction
:
public interface ISerialNumberProvider
{
string GetSerialNumber();
}
public sealed class RandomSerialNumberProvider : ISerialNumberProvider
{
public string GetSerialNumber() => $"Serial-Number-{Random.Shared.Next()}";
}
由于我使用的是 ASP.NET 核心,因此我依靠 Microsoft DI 来组成我的所有类:
public static class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddAutoMapper(typeof(StudentProfile));
builder.Services.AddSingleton<ISerialNumberProvider, RandomSerialNumberProvider>();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
有了这种设置,我能够将 IMapper
服务注入到我的控制器类中,并且一切正常。以下是一个例子:
[ApiController]
[Route("api/[controller]")]
public class StudentsController : ControllerBase
{
private static readonly ImmutableArray<Student> s_students =
[
new Student { Id = 1, BirthDate = new DateOnly(1988, 2, 6), FirstName = "Bob", LastName = "Brown" },
new Student { Id = 2, BirthDate = new DateOnly(1991, 8, 4), FirstName = "Alice", LastName = "Red" },
];
private readonly IMapper _mapper;
public StudentsController(IMapper mapper)
{
_mapper = mapper;
}
[HttpGet]
public IEnumerable<StudentDto> Get()
{
return s_students
.Select(_mapper.Map<StudentDto>)
.ToArray();
}
}
上面写的代码运行良好,唯一的问题是在单元测试时。我将要描述的问题的根本原因是我依赖 Microsoft DI 来实例化类 Profile
、映射操作 SetSerialNumberAction
及其依赖项 ISerialNumberProvider
.
这是我通常为 AutoMapper 的类编写测试的方式 Profile
(此示例用作 XUnit
测试框架):
public sealed class MappingUnitTests
{
private readonly MapperConfiguration _configuration;
private readonly IMapper _sut;
public MappingUnitTests()
{
_configuration = new MapperConfiguration(config =>
{
config.AddProfile<StudentProfile>();
});
_sut = _configuration.CreateMapper();
}
[Fact]
public void Mapper_Configuration_Is_Valid()
{
// ASSERT
_configuration.AssertConfigurationIsValid();
}
[Fact]
public void Property_BirthDate_Is_Assigned_Right_Value()
{
// ARRANGE
var student = new Student
{
FirstName = "Mario",
LastName = "Rossi",
Id = 1,
BirthDate = new DateOnly(1980, 1, 5)
};
// ACT
var result = _sut.Map<StudentDto>(student);
// ASSERT
Assert.NotNull(result);
Assert.Equal(student.BirthDate, result.BirthDate);
}
[Fact]
public void Property_FullName_Is_Assigned_Right_Value()
{
// ARRANGE
var student = new Student
{
FirstName = "Mario",
LastName = "Rossi",
Id = 1,
BirthDate = new DateOnly(1980, 1, 5)
};
// ACT
var result = _sut.Map<StudentDto>(student);
// ASSERT
Assert.NotNull(result);
Assert.Equal("Mario Rossi", result.FullName);
}
}
这里有两个不同的问题:
Property_BirthDate_Is_Assigned_Right_Value
均 Property_FullName_Is_Assigned_Right_Value
失败,错误为 System.MissingMethodException:无法动态创建类型为“AutomapperUnitTestingSample.Mapping.SetSerialNumberAction”的实例。原因:未定义无参数构造函数。
ISerialNumberProvider
在我的测试中注入模拟实例以便验证属性是否 StudentDto.SerialNumber
通过使用映射操作正确映射 SetSerialNumberAction
。
上述描述的问题可以通过在单元测试中使用 Microsoft DI 来解决。
例如,下面的测试类可以正常工作并解决上面描述的两个问题:
public sealed class MappingUnitTestsWithContainer : IDisposable
{
private readonly IMapper _sut;
private readonly ServiceProvider _serviceProvider;
private readonly Mock<ISerialNumberProvider> _mockSerialNumberProvider;
public MappingUnitTestsWithContainer()
{
// init mock
_mockSerialNumberProvider = new Mock<ISerialNumberProvider>();
// configure services
var services = new ServiceCollection();
services.AddAutoMapper(typeof(StudentProfile));
services.AddSingleton<ISerialNumberProvider>(_mockSerialNumberProvider.Object);
// build service provider
_serviceProvider = services.BuildServiceProvider();
// create sut
_sut = _serviceProvider.GetRequiredService<IMapper>();
}
[Fact]
public void Mapper_Configuration_Is_Valid()
{
// ASSERT
_sut.ConfigurationProvider.AssertConfigurationIsValid();
}
[Fact]
public void Property_BirthDate_Is_Assigned_Right_Value()
{
// ARRANGE
var student = new Student
{
FirstName = "Mario",
LastName = "Rossi",
Id = 1,
BirthDate = new DateOnly(1980, 1, 5)
};
// ACT
var result = _sut.Map<StudentDto>(student);
// ASSERT
Assert.NotNull(result);
Assert.Equal(student.BirthDate, result.BirthDate);
}
[Fact]
public void Property_FullName_Is_Assigned_Right_Value()
{
// ARRANGE
var student = new Student
{
FirstName = "Mario",
LastName = "Rossi",
Id = 1,
BirthDate = new DateOnly(1980, 1, 5)
};
// ACT
var result = _sut.Map<StudentDto>(student);
// ASSERT
Assert.NotNull(result);
Assert.Equal("Mario Rossi", result.FullName);
}
[Fact]
public void Property_SerialNumber_Is_Set_By_Using_SetSerialNumberAction()
{
// ARRANGE
var student = new Student
{
FirstName = "Mario",
LastName = "Rossi",
Id = 1,
BirthDate = new DateOnly(1980, 1, 5)
};
// setup mock
_mockSerialNumberProvider
.Setup(m => m.GetSerialNumber())
.Returns("serial-number-123");
// ACT
var result = _sut.Map<StudentDto>(student);
// ASSERT
Assert.NotNull(result);
Assert.Equal("serial-number-123", result.SerialNumber);
}
public void Dispose()
{
_serviceProvider.Dispose();
}
}
这个问题还有其他解决方案吗? 是否可以通过在单元测试类中不使用 DI 容器来解决此问题?
作为参考,我创建了一个 GitHub 存储库 ,其中包含用于展示此问题的工作代码。