Data Mapper
The Data Mapper
pattern is an architectural pattern that aims to separate the in-memory objects from the database.
It does this by creating a dedicated layer (the Data Mapper/Data Access) responsible for transferring data between the two.
Pros
Improves testebility. By separating data access logic from
domain objects
,unit tests
for thebusiness logic
can be easily written without relying on a real database.Changes to database schema or data access strategies can be made within the
Mapper
without impacting other parts of the application.Allows easily switch between different database (e.g., from MS SQL to PostgreSQL) without affecting the
domain logic
.
Cons
Introduces an extra layer of abstraction, making the system slightly more complex to understand and implement compared to simpler approaches.
Requires careful design and implementation to avoid over-engineering and potential performance bottlenecks.
Example
namespace ApplicationLayer {
using DomainLayer;
using DataAccessLayer;
public class ProductService(string connectionString)
{
public async Task Ex() {
ProductMapper mapper = new(connectionString);
// Get all products
IList<Product> allProducts = await mapper.GetAllAsync();
// Get a specific product
Product product = await mapper.GetByIdAsync(1) ?? throw new InvalidOperationException("Product not found.");
product.ApplyDiscount(30);
await mapper.SaveAsync(product);
}
}
}
namespace DomainLayer
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
// Domain business logic
public void ApplyDiscount(decimal discountPercent)
{
this.Price = Price - (Price * discountPercent);
}
}
}
namespace DataAccessLayer
{
using DomainLayer;
using Microsoft.Data.SqlClient;
using System.Collections.Generic;
using System.Threading.Tasks;
public class ProductMapper(string connectionString)
{
public async Task<List<Product>> GetAllAsync()
{
var products = new List<Product>();
using SqlConnection connection = new(connectionString);
await connection.OpenAsync();
using SqlCommand command = new("SELECT Id, Name, Price FROM Products", connection);
using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
products.Add(new Product
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Price = reader.GetDecimal(2)
});
}
return products;
}
public async Task<Product?> GetByIdAsync(int id)
{
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
using var command = new SqlCommand("SELECT Id, Name, Price FROM Products WHERE Id = @Id", connection);
command.Parameters.AddWithValue("@Id", id);
using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return new Product
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Price = reader.GetDecimal(2)
};
}
return null;
}
public async Task<bool> SaveAsync(Product product)
{
using SqlConnection connection = new(connectionString);
await connection.OpenAsync();
using var command = new SqlCommand(
product.Id == 0
? "INSERT INTO Products (Name, Price) VALUES (@Name, @Price);"
: "UPDATE Products SET Name = @Name, Price = @Price WHERE Id = @Id;",
connection);
command.Parameters.AddWithValue("@Name", product.Name);
command.Parameters.AddWithValue("@Price", product.Price);
if (product.Id != 0)
{
command.Parameters.AddWithValue("@Id", product.Id);
}
int rowsAffected = await command.ExecuteNonQueryAsync();
return rowsAffected > 0;
}
}
}
Data mapper implementation
Third-party libraries help to simplify the creation of a Data Mapper layer.
C# - EF Core
Acts as a more comprehensive Data Mapper, handling a significant portion of the data mapping logic internally.
It translates LINQ queries into SQL,
tracks changes
to entities, and manages relationships between objects.The mapping process can be customize using fluent API or data annotations.
Example. EF Core Data Mapper
namespace ApplicationLayer
{
using DataAccessLayer;
using DomainLayer;
public class ProductService(MyDbContext myDbContext)
{
public async Task Ex()
{
ProductMapper mapper = new(myDbContext);
// Get all products
IList<Product> allProducts = await mapper.GetAllAsync();
// Get a specific product
Product product = await mapper.GetByIdAsync(1) ?? throw new InvalidOperationException("Product not found.");
product.ApplyDiscount(30);
await mapper.UpdateAsync(product);
}
}
}
namespace DomainLayer
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
// Domain business logic
public void ApplyDiscount(decimal discountPercent)
{
this.Price = Price - (Price * discountPercent);
}
}
public interface IProductMapper
{
Task<List<Product>> GetAllAsync();
Task<Product?> GetByIdAsync(int id);
Task<Product> CreateAsync(Product product);
Task<bool> UpdateAsync(Product product);
Task<bool> DeleteAsync(int id);
}
}
namespace DataAccessLayer
{
using DomainLayer;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
public class ProductMapper(MyDbContext context) : IProductMapper
{
public async Task<List<Product>> GetAllAsync()
{
return await context.Products.ToListAsync();
}
public async Task<Product?> GetByIdAsync(int id)
{
return await context.Products.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task<Product> CreateAsync(Product product)
{
context.Products.Add(product);
await context.SaveChangesAsync();
return product;
}
public async Task<bool> UpdateAsync(Product product)
{
context.Products.Update(product);
await context.SaveChangesAsync();
return true;
}
public async Task<bool> DeleteAsync(int id)
{
var product = await context.Products.FindAsync(id);
if (product == null)
{
return false;
}
context.Products.Remove(product);
await context.SaveChangesAsync();
return true;
}
}
public class MyDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
// ... (Constructor and DbContextOptions configuration) ...
}
}
C# - Dapper
- A more lightweight
Data Mapper
thenEF Core
. - There is more control over the mapping process, code is written with raw SQL queries and the results are manually mapped to .NET objects.
- While less automated, it offers greater flexibility and can be more performant for certain scenarios.
Example. Dapper Data Mapper
using Dapper;
using Microsoft.Data.SqlClient;
namespace DomainLayer
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public interface IProductMapper
{
Task<IEnumerable<Product>> GetAllAsync();
Task<Product?> GetByIdAsync(int id);
Task<Product> CreateAsync(Product product);
Task<bool> UpdateAsync(Product product);
Task<bool> DeleteAsync(int id);
}
}
namespace DataAccessLayer
{
using DomainLayer;
public class ProductMapper(string connectionString) : IProductMapper
{
public async Task<IEnumerable<Product>> GetAllAsync()
{
string sql = "SELECT Id, Name, Price FROM Products";
using SqlConnection connection = new(connectionString);
var products = await connection.QueryAsync<Product>(sql);
return products;
}
public async Task<Product?> GetByIdAsync(int id)
{
string sql = "SELECT Id, Name, Price FROM Products WHERE Id = @Id";
using SqlConnection connection = new(connectionString);
return await connection.QueryFirstOrDefaultAsync<Product>(sql, new { Id = id });
}
public async Task<Product> CreateAsync(Product product)
{
string sql = "INSERT INTO Products (Name, Price) OUTPUT INSERTED.Id VALUES (@Name, @Price)";
using SqlConnection connection = new(connectionString);
product.Id = await connection.ExecuteScalarAsync<int>(sql, product);
return product;
}
public async Task<bool> UpdateAsync(Product product)
{
string sql = "UPDATE Products SET Name = @Name, Price = @Price WHERE Id = @Id";
using SqlConnection connection = new(connectionString);
return await connection.ExecuteAsync(sql, product) > 0;
}
public async Task<bool> DeleteAsync(int id)
{
string sql = "DELETE FROM Products WHERE Id = @Id";
using SqlConnection connection = new(connectionString);
return await connection.ExecuteAsync(sql, new { Id = id }) > 0;
}
}
}
Typescript/JavaScript - TypeOrm
TypeOrm
is ORM designed for Node.js
and TypeScript
environments.
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class Product {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column('decimal', { precision: 10, scale: 2 })
price: number;
}
import { EntityRepository, Repository } from 'typeorm';
@EntityRepository(Product)
export class ProductMapper extends Repository<Product> {
async getAll(): Promise<Product[]> {
return await this.find();
}
async getById(id: number): Promise<Product | null> {
return await this.findOneBy({ id });
}
async create(product: Product): Promise<Product> {
return await this.save(product);
}
async update(product: Product): Promise<boolean> {
const result = await this.save(product);
return !!result;
}
async deleteById(id: number): Promise<boolean> {
const result = await this.delete({ id });
return result.affected > 0;
}
}