services.mapping.examples - grecosoft/NetFusion GitHub Wiki
The following will show examples of mapping domain entities to models. These examples will use manual mappings with Linq and the TinyMapper open source mapping library for demonstration purposes. While Linq is not mapping specific, it can be useful for coding mappings.
The following code creates the needed entities and models that will be used by the mapping examples.
Within the Examples.Mapping.Domain project, add the following class definitions to an Entities directory.
using System.Collections.Generic;
using NetFusion.Common.Base.Entity;
namespace Examples.Mapping.Domain.Entities
{
public class Student : IAttributedEntity
{
private readonly List<int> _scores = new List<int>();
public string FirstName { get; }
public string LastName { get; }
public int[] Scores => _scores.ToArray();
public bool Passing { get; set; }
public Student(string firstName, string lastName, IEnumerable<int> scores)
{
_scores.AddRange(scores);
Attributes = new EntityAttributes();
FirstName = firstName;
LastName = lastName;
}
public IEntityAttributes Attributes { get; }
public IDictionary<string, object?> AttributeValues
{
get => Attributes.GetValues();
set => Attributes.SetValues(value);
}
public void AddScore(int score)
{
_scores.Add(score);
}
}
}
using System.Collections.Generic;
namespace Examples.Mapping.Domain.Entities
{
public class Course
{
public string Name { get; }
public string Instructor { get; }
public int Year { get; }
public int Semester { get; set; }
public IReadOnlyCollection<Student> Students => _students;
private readonly List<Student> _students;
public Course(
string name,
string instructor,
int year,
int semester)
{
Name = name;
Instructor = instructor;
Year = year;
Semester = semester;
_students = new List<Student>();
}
public void AddStudent(Student student)
{
_students.Add(student);
}
}
}
Add the following service to the Examples.Mapping.App project to return hard-coded entity data:
using Examples.Mapping.Domain.Entities;
namespace Examples.Mapping.App.Services;
public class SampleEntityService
{
private static Course ExampleEntity =>
new Course("Computer Science",
"Mark Smith",
2018,
2);
private static void AddStudents(Course course)
{
course.AddStudent(
new Student("Tom", "Green", new[] { 100, 92 }));
course.AddStudent(
new Student("Jim", "Smith", new[] { 90, 89 }));
}
// Methods returning entity for use by mappings examples:
public Course GetCourse()
{
var course = ExampleEntity;
AddStudents(course);
return course;
}
}
Register the service within the following module: Examples.Mapping.App/Plugin/Modules/ServiceModule.cs
nano ./src/Components/Demo.App/Plugin/Modules/ServiceModule.cs
using Examples.Mapping.App.Services;
using Microsoft.Extensions.DependencyInjection;
using NetFusion.Core.Bootstrap.Plugins;
namespace Examples.Mapping.App.Plugin.Modules;
public class ServiceModule : PluginModule
{
public override void RegisterServices(IServiceCollection services)
{
services.AddScoped<SampleEntityService>();
}
}
These will be the models into which the domain-entities are mapped and returned by the WebApi method. Add the following classes to the Models directory of the Examples.Mapping.WebApi project:
namespace Examples.Mapping.WebApi.Models;
public class StudentAverage
{
public string Name { get; set; } = string.Empty;
public double AverageScore { get; set; }
}
namespace Examples.Mapping.WebApi.Models;
public class StudentListingSummary
{
public string Instructor { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public int Year { get; set; }
public int Semester { get; set; }
public int HighestScore { get; set; }
public StudentAverage[] Averages { get; set; } = Array.Empty<StudentAverage>();
}
This example shows a typical mapping example specified using C# code without the use of an open-source mapping library. This is the best choice when performance is the greatest importance or a dependency on an open-source mapping library is not desired.
Add the following mapping strategy implemented with Linq to the Mappings directory of the Examples.Mapping.WebApi project:
using Examples.Mapping.Domain.Entities;
using Examples.Mapping.WebApi.Models;
using NetFusion.Services.Mapping;
namespace Examples.Mapping.WebApi.Mappings;
public class StudentListMapping : MappingStrategy<Course, StudentListingSummary>
{
protected override StudentListingSummary SourceToTarget(Course source)
{
return new StudentListingSummary {
Instructor = source.Instructor,
Name = source.Name,
Year = source.Year,
Semester = source.Semester,
HighestScore = source.Students
.SelectMany(s => s.Scores)
.Max(),
Averages = source.Students
.Select(s => new StudentAverage
{
Name = s.FirstName + s.LastName,
AverageScore = s.Scores.Average()
})
.ToArray()
};
}
}
The above mapping strategy was created by deriving from the base generic MappingStrategy class. When deriving from this base class, the source and target types are specified and the SourceToTarget method is overridden.
Add the following controller to the Examples.Mapping.WebApi project to test the mapping examples starting in the next section:
using Examples.Mapping.App.Services;
using Examples.Mapping.WebApi.Models;
using Microsoft.AspNetCore.Mvc;
using NetFusion.Services.Mapping;
namespace Examples.Mapping.WebApi.Controllers;
[ApiController, Route("api/[controller]")]
public class ExamplesController : ControllerBase
{
private readonly SampleEntityService _entityService;
private readonly IObjectMapper _objectMapper;
public ExamplesController(
SampleEntityService entityService,
IObjectMapper objectMapper)
{
_entityService = entityService;
_objectMapper = objectMapper;
}
[HttpGet("code-mapping")]
public StudentListingSummary GetStudentListingSummary()
{
var courseEntity = _entityService.GetCourse();
return _objectMapper.Map<StudentListingSummary>(courseEntity);
}
}
Run the example WebApi service and test the code by calling the controller's method:
cd ./src/Examples.Mapping.WebApi
dotnet run
http://localhost:5010/api/examples/code-mapping
-
During the bootstrap process, the NetFusion.Mapping plug-in locates all IMappingStrategy types and stores a lookup in memory.
-
The MappingStrategy base class implements the IMappingStrategy interface and takes generic parameters specifying the associated source and target types. This class provides the following method and property used to specify the mappings:
- SourceToTarget: Is passed the source object and returns an instance of the corresponding target object.
- Mapper: This property is of type IObjectMapper and contains a reference to the service used to map one object to another. This can be used within the above mapping method to map objects related to the source or targets objects being mapped within the strategy.
Mapping is preformed by the IObjectMapper instance and can be injected into the component needing to apply a mapping.
Following is an example of mapping a source object to a specific target type:
var courseEntity = _entityService.GetCourse();
return _objectMapper.Map<StudentListingSummary>(courseEntity);
The implementation determines the mapping strategy to use for mapping a source to a target object as follows:
- A strategy where the associated source and target types are an exact match is first searched. If found, the strategy is used and the SourceToTarget method is called.
- If no mapping strategy is found, a strategy for the specified source type having an associated derived target type is searched. If so, the SourceToTarget method is invoked. This scenario is illustrated by next example.
- If no mapping can be determined, an exception is thrown.
- If more than one mapping is found, an exception is thrown.
Note: The IObjectMapper also provides TryMap which will attempt to map the entity and returns a boolean value indicating if the mapping was successful. If True is returned, the mapped object is returned as an out parameter.
The next example shows how target types, deriving from a common base type, can be mapped into based on the source type. This is useful when mappings are polymorphic. For example, you can have a list of contacts (student, customer, employee, ... entities) and for each source type there is an associated model (StudentSummary, CustomerSummary, and ConsumerSummary) each driving from a common base type: ContactSummary. Using this approach you can map the collection of derived contacts to their corresponding derived Summary models.
Add the following models to the Models directory of the Examples.Mapping.WebApi project:
namespace Examples.Mapping.WebApi.Models;
public abstract class ContactSummary
{
public string? FullName { get; set; }
}
namespace Examples.Mapping.WebApi.Models;
public class StudentSummary : ContactSummary
{
public int MaxScore { get; set; }
public int MinScore { get; set; }
}
namespace Demo.WebApi.Models
{
public class TeacherSummary : ContactSummary
{
public string State { get; set; }
public string Zip { get; set; }
}
}
Create additional example entity:
namespace Examples.Mapping.Domain.Entities;
public class Teacher
{
public int TeacherId { get; }
public string FirstName { get; }
public string LastName { get; }
public string Address { get; }
public string City { get; }
public string State { get; }
public string Zip { get; }
public Teacher(int teacherId, string firstName, string lastName,
string address,
string city,
string state,
string zip)
{
TeacherId = teacherId;
FirstName = firstName;
LastName = lastName;
Address = address;
City = city;
State = state;
Zip = zip;
}
}
Create the following Mapping Strategies:
using Examples.Mapping.Domain.Entities;
using Examples.Mapping.WebApi.Models;
using NetFusion.Services.Mapping;
namespace Examples.Mapping.WebApi.Mappings;
public class StudentContactMapping: MappingStrategy<Student, StudentSummary>
{
protected override StudentSummary SourceToTarget(Student source) =>
new()
{
FullName = source.FirstName + " == " + source.LastName,
MaxScore = source.Scores.Max(),
MinScore = source.Scores.Min()
};
}
using Examples.Mapping.Domain.Entities;
using Examples.Mapping.WebApi.Models;
using NetFusion.Services.Mapping;
namespace Examples.Mapping.WebApi.Mappings;
public class TeacherContactMapping: MappingStrategy<Teacher, TeacherSummary>
{
protected override TeacherSummary SourceToTarget(Teacher source) =>
new()
{
FullName = source.FirstName + " " + source.LastName,
State = source.State,
Zip = source.Zip
};
}
Add the following method and required namespaces to the SampleEntityService created above:
public object[] GetContacts() {
var entity = ExampleEntity;
AddStudents(entity);
var student = entity.Students.First();
var teacher = new Teacher(1, "Ben", "Smith",
"134 West Main",
"Cheshire",
"CT",
"06410");
return new object[]
{
student,
teacher
};
}
Add the following method to the ExamplesController created above:
[HttpGet("derived-targets")]
public IActionResult GetContactSummaries()
{
var contacts = _entityService.GetContacts();
var mappedResults = contacts.Select(
c => _objectMapper.Map<ContactSummary>(c)
).ToArray();
return Ok(mappedResults.Cast<object>().ToArray());
}
Run Web Api service and Test by executing the following requests:
cd ./src/Examples.Mapping.WebApi
dotnet run
http://localhost:5010/api/examples/derived-targets
Note in this example, both the student and customer are being mapped to target types deriving from ContactSummary. When the above mapping is executed in the controller, the corresponding derived ContactSummary mappings will be used. In this case the StudentContactMapping and the CustomerContactMappings will be used respectively.
After the mapping strategy type has been determined, it is instantiated from the container's current lifetime. This allows the mapping strategy to dependency-inject any needed services. This should not be abused since the majority of the data should be contained on the source object being mapped. However, there are times when this can be useful. The method invoked on the injected service must be synchronous. If an asynchronous method is needed, the call should be placed in the calling code before the mapping is invoked. The returned data from the asynchronous call can then be added to the source object being mapped. For a simple example, the following injects a service implementing the IEntityIdGenerator interface used to obtain a generated identity value. A more realistic example would be injecting common formatting functionality.
Add the following models to the Models directory of the Examples.Mapping.WebApi project:
namespace Examples.Mapping.WebApi.Models;
public class StudentCalcSummary
{
public string? StudentId { get; set; }
public string? FullName { get; set; }
public IDictionary<string, object>? Calculations { get; set; }
}
Create interface for service to be Injected within the Examples.Mapping.Domain/Services directory:
namespace Examples.Mapping.Domain.Services;
public interface IEntityIdGenerator
{
string GenerateId();
}
Then add the Implementation to the following directory: Examples.Mapping.App/Services:
using System;
using Examples.Mapping.Domain.Services;
namespace Examples.Mapping.App.Services;
public class EntityIdGenerator : IEntityIdGenerator
{
public string GenerateId()
{
return Guid.NewGuid().ToString();
}
}
Register the service by adding the following lines within the RegisterServices to the the following file:
Examples.Mapping.App/Plugin/Modules/ServiceModule.cs
using Examples.Mapping.App.Services;
using Examples.Mapping.Domain.Services;
using Microsoft.Extensions.DependencyInjection;
using NetFusion.Core.Bootstrap.Plugins;
namespace Examples.Mapping.App.Plugin.Modules;
public class ServiceModule : PluginModule
{
public override void RegisterServices(IServiceCollection services)
{
services.AddScoped<SampleEntityService>();
services.AddScoped<IEntityIdGenerator, EntityIdGenerator>(); // <-- Add this line
}
}
Create the following mapping strategy injecting the IEntityIdGenerator service:
using Examples.Mapping.Domain.Entities;
using Examples.Mapping.Domain.Services;
using Examples.Mapping.WebApi.Models;
using NetFusion.Services.Mapping;
namespace Examples.Mapping.WebApi.Mappings;
public class StudentCalcMapping: MappingStrategy<Student, StudentCalcSummary>
{
private IEntityIdGenerator IdGenerator { get; }
public StudentCalcMapping(IEntityIdGenerator idGenerator)
{
IdGenerator = idGenerator;
}
protected override StudentCalcSummary SourceToTarget(Student source)
{
var summary = new StudentCalcSummary
{
StudentId = IdGenerator.GenerateId(),
FullName = source.FirstName + " " + source.LastName,
Calculations = source.AttributeValues
};
return summary;
}
}
Add the following method to the SampleEntityService that was created above:
public Student GetStudent()
{
var entity = ExampleEntity;
AddStudents(entity);
var student = entity.Students.First();
student.Attributes.Values.MaxScore = student.Scores.Max();
student.Attributes.Values.MinScore = student.Scores.Min();
return student;
}
Add the following method to the ExamplesController created above:
[HttpGet("dependency-mapping")]
public StudentCalcSummary GetStudentSummary()
{
var student = _entityService.GetStudent();
return _objectMapper.Map<StudentCalcSummary>(student);
}
Run Web Api service and Test by executing the following URL:
cd ./src/Examples.Mapping.WebApi
dotnet run
http://localhost:5010/api/examples/dependency-mapping
The following shows an example of how to delegate mapping to an open-source mapping library. For mappings where the target type is a subset of the source type and only a simple one-to-one mapping is needed, the following method can be used to reduce the number of mapping classes. This example uses the open-source TinyMapper library as an example.
dotnet add ./src/Examples.Mapping.WebApi/Examples.Mapping.WebApi.csproj package TinyMapper
namespace Demo.WebApi.Models
{
public class CarSummary
{
public string Make { get; set; }
public string Model { get; set; }
public int Year { get; set; }
public decimal Price { get; set;}
}
}
Add the following entity to the Examples.Mapping.Domain project:
namespace Examples.Mapping.Domain.Entities;
public class Car
{
public string Make { get; }
public string Model { get; }
public decimal Price { get; }
public string Color { get; }
public int Year { get;}
public Car(string make, string model, decimal price, string color, int year)
{
Make = make;
Model = model;
Price = price;
Color = color;
Year = year;
}
// Don't expose theses on the model :)
public bool HasSalvageTitle { get; init; }
public bool WasSmokerCar { get; init;}
}
Add the below mapping class to the mappings directory:
using Examples.Mapping.Domain.Entities;
using Examples.Mapping.WebApi.Models;
using Nelibur.ObjectMapper;
using NetFusion.Services.Mapping;
namespace Examples.Mapping.WebApi.Mappings;
public class EntityMappingStrategyFactory : IMappingStrategyFactory
{
static EntityMappingStrategyFactory()
{
TinyMapper.Bind<Car, CarSummary>();
}
public IEnumerable<IMappingStrategy> GetStrategies()
{
yield return DelegateMap.Map((Car c) => TinyMapper.Map<CarSummary>(c));
}
}
Add the following method to the SampleEntityService that was created above:
public Car GetCar()
{
return new Car {
Make = "Honda",
Model = "Accord",
Year = 2010,
Price = 8000,
WasSmokerCar = true,
HasSalvageTitle = true
};
}
Add the following method to the ExamplesController created above:
[HttpGet("opensource-mapping")]
public CarSummary GetStudentCalcSummary()
{
var carEntity = _entityService.GetCar();
return _objectMapper.Map<CarSummary>(carEntity);
}
Run Web Api service and Test by calling the following URL:
cd ./src/Examples.Mapping.WebApi
dotnet run
http://localhost:5010/api/examples/opensource-mapping
Another advantage of implementing IMappingStrategyFactory over deriving from MappingStrategy is that multiple mappings can be specified within a single file.
Create the following two entities:
using System;
namespace Examples.Mapping.Domain.Entities;
public class Computer
{
public string Make { get; }
public string Model { get; }
public int Ram { get; }
public DateTime DateBuilt { get; }
public Computer(string make, string model, int ram, DateTime dateBuilt)
{
Make = make;
Model = model;
Ram = ram;
DateBuilt = dateBuilt;
}
}
namespace Examples.Mapping.Domain.Entities;
public class Printer
{
public string Make { get; }
public string Model { get; }
public string Description { get; }
public decimal Price { get; }
public bool IsColor { get; init; }
public Printer(string make, string model, string description, decimal price)
{
Make = make;
Model = model;
Description = description;
Price = price;
}
}
Create the following WebApi model:
namespace Examples.Mapping.WebApi.Models;
public class ProductSummary
{
public string? Make { get; set; }
public string? Model { get; set; }
public string? Description { get; set; }
}
The create the following mapping to convert each entity to the common ProductSummary model:
using Examples.Mapping.Domain.Entities;
using Examples.Mapping.WebApi.Models;
using NetFusion.Services.Mapping;
namespace Examples.Mapping.WebApi.Mappings;
public class ProductMappings : IMappingStrategyFactory
{
public IEnumerable<IMappingStrategy> GetStrategies()
{
yield return DelegateMap.Map((Computer entity) => new ProductSummary
{
Make = entity.Make,
Model = entity.Model,
Description = $"Ram: {entity.Ram}, Date Built: {entity.DateBuilt}"
});
yield return DelegateMap.Map((Printer entity) => new ProductSummary
{
Make = entity.Make,
Model = entity.Model,
Description = $"Color: {entity.IsColor}, Price: {entity.Price}"
});
}
}
The DelegateMap is a small helper class that will return instances based on the source and target types inferred from the provided mapping delegates. DelegateMap provides the following two methods:
- Map: Used to provide a one-way mapping between types. There is also an override passed the IObjectMapper that can be referenced to map child classes referenced by the class being mapped.
Note: One difference between implementing IMappingStrategyFactory and deriving from MappingStrategy is that a single cached instance of the IMappingStrategy is returned from the factory and not created per-request. Therefore they cannot have services injected.
The following shows a request being made to the following URL: http://localhost:5010/api/examples/factory-mapping