我正在使用 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 存储库 ,其中包含用于展示此问题的工作代码。
我正在对与 SolidEdge API 交互的方法进行单元测试。Solid Edge API 是一个 COM 组件,用于自动执行 3D 模型更改,如 AutoCAD。模拟数据只能返回一次。之后它会重新...
交互的单元测试方法 SolidEdge api 。 Solid Edge API 是一个 COM 组件,用于像 AutoCAD 一样自动化 3D 模型更改。
模拟数据只能返回一次。之后它将返回 null。
所有模拟都运行良好,但仅在第一次调用时有效。因此,我从 includeIn1 获得空值 includeIn2 。即使两者都访问相同的属性。
public static ResultModel GetMockedAssemblyDocument(MockRequestModel mockRequestModel)
{
var mockAssemblyDocument = new Mock<AssemblyDocument>();
var mockOccurrences = new Mock<Occurrences>();
var mockOccurrenceList = new List<Mock<Occurrence>>();
foreach (var occurrence in mockRequestModel.OccurrencesDetails)
{
var mockOccurrence = MockSolidEdgeDocumentOccurrence(occurrence.OccurrenceName, occurrence.Properties, occurrence.Variables);
mockOccurrenceList.Add(mockOccurrence);
}
mockOccurrences.Setup(o => o.GetEnumerator()).Returns(mockOccurrenceList.Select(mock => mock.Object).GetEnumerator());
mockAssemblyDocument.Setup(doc => doc.Occurrences).Returns(mockOccurrences.Object);
return new ResultModel { MockedAssemblyDocument = mockAssemblyDocument, MockedAssemblyDocumentCount = 2 };
}
public static Mock<Occurrence> MockSolidEdgeDocumentOccurrence(string occurrenceName, Dictionary<string, object> properties,
Dictionary<string, VariableInfo> variables)
{
// Existing code
mockOccurrence1.SetupGet(o => o.OccurrenceDocument).Returns(() =>
{
var mockSolidEdgeDocument = new Mock<SolidEdgeDocument>();
var mockPropertySets = GetPropertySetsMock(properties);
mockSolidEdgeDocument.Setup(doc => doc.Properties).Returns(mockPropertySets.Object);
return mockSolidEdgeDocument.Object;
});
return mockOccurrence1;
}
private static Mock<PropertySets> GetPropertySetsMock(Dictionary<string, object> properties)
{
var mockPropertySets = new Mock<PropertySets>();
var mockPropertySet = new Mock<Properties>();
var propertyObjectType = new List<dynamic>();
foreach (var property in properties)
{
dynamic dynamicProperty = new ExpandoObject();
dynamicProperty.Name = property.Key;
dynamicProperty.Value = property.Value;
propertyObjectType.Add(dynamicProperty);
}
mockPropertySet.Setup(ps => ps.GetEnumerator()).Returns(propertyObjectType.GetEnumerator());
mockPropertySets.Setup(ps => ps.GetEnumerator()).Returns(new List<Properties> { mockPropertySet.Object }.GetEnumerator());
return mockPropertySets;
}
[Test]
public void GetSupportData_ShouldReturnListOfComponents()
{
// Arrange
var mockRequestModel = new MockRequestModel
{
OccurrencesDetails = new List<MockOccurrenceRequestModel>
{
new()
{
OccurrenceName = "Occurrence1",
Properties = new Dictionary<string, object>()
{
{ nameof(FilePropertyNameConstants.ObjectType), nameof(ObjectTypeConstants.Elbow) },
{ nameof(FilePropertyNameConstants.IncludeIn), "Yes" }
}
}
}
};
var mockAssemblyDocument = GetMockedAssemblyDocument(mockRequestModel);
_mockConnectionRepository.Setup(repo => repo.GetAssemblyDocument()).Returns(mockAssemblyDocument.MockedAssemblyDocument.Object);
// Act
var result = _isometricRepository.GetData();
// Assert
Assert.That(result, Is.Not.Null);
}
public List<ResultModel> GetData()
{
// Existing code
SolidEdgeDocument occurrenceDocument = occurrence.OccurrenceDocument;
if (!occurrenceDocument.PropertyValueExists(FilePropertyNameConstants.IncludeIn, out object? includeIn1)) continue;
if (!occurrenceDocument.PropertyValueExists(FilePropertyNameConstants.IncludeIn, out object? includeIn2)) continue;
// Existing code
}
@MohammadAghazadeh 我并不是想测试库内部,而是想测试提供给库本身的映射配置。
这个问题还有其他的解决方案吗?
是的。只需不要在 AutoMapper 中使用 DI。
但是...我想在 AutoMapper 中使用 DI。
那么抱歉,但你使用 AutoMapper 的方式是错误的。也许你现在没有看到它,但如果你在接下来的项目中继续使用 AutoMapper,你将来会看到它。
正如 AutoMapper 的作者 Jimmy Bogard 在关于 AutoMapper 的设计理念的 :
AutoMapper 之所以有效,是因为它强制执行了一种约定。它假设您的目标类型是源类型的子集。它假设您的目标类型上的所有内容都应进行映射。它假设目标成员名称遵循源类型的确切名称。它假设您希望将复杂模型扁平化为简单模型。
对我来说,它还假设您要映射的内容很简单,但无处不在,因此您不想手动执行该过程,因为需要编写太多无聊的代码。充分利用 AutoMapper 并避免产生令人讨厌的副作用的关键是保持简单。知道何时使用它,何时不使用,并认识到您应该选择可单元测试、定制、手动方法而不是 AutoMapper 的时刻。
我知道 AutoMapper 本身支持 DI,但在我看来,当你需要 DI 来映射某些模型时,恐怕它就不再简单了。请记住,它应该是简单的,让你的项目处于比使用该库之前更好的状态。保持简单可以让你的代码库可维护性保持在较低水平。
另一方面,让自定义解析器、DI、后映射或前映射在您的配置文件周围疯狂增长,会给您或在您之后试图扩展或修复您留下的代码的程序员带来麻烦。将您的业务逻辑(是的,设置序列号是业务逻辑)放入您的映射配置文件中是一个错误,它会影响代码的可维护性。您已经经历过这种情况,因为您已经注意到测试这种映射配置有多么困难。现在想象一下,试图在一个一半业务逻辑分散在映射配置文件周围的项目中寻找一些错误。我见过这样的案例,相信我,这是一场噩梦。StackOverflow 本身就充满了这样的代码问题。
另外,请查看 Jimmy 的 AutoMapper 使用指南 。值得一读。
AutoMapper 具有与其他映射库不同的独特功能,即 配置验证 集成测试 ,则效果最佳 。使用简单的映射配置文件,这就是您所需要的一切。AutoMapper 将自动检测何时有人添加了新属性,而现在缺少该属性的映射。与 CI 配合使用,这将使您对模型具有很高的控制力。
public class AutoMapperTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public AutoMapperTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Fact]
public void Should_Not_Throw_WithValidConfiguration()
{
// Assert
// Get mapper from service provider.
// DO NOT create it manually if your mapping profiles use DI.
// Act
var exception = Record.Exception(() => mapper.ConfigurationProvider.AssertConfigurationIsValid());
// Assert
Assert.Null(exception);
}
}
所以,这确实是一个很长的话题...但是,在你的场景中你应该采取什么实际步骤呢?
p12
p13