החזרת DTO

כשיש לנו פער בין האובייקט שאנחנו מקבלים מה-DB והמידע שאנחנו צריכים במד הסרבר לבין המידע שבסופו של דבר עובר ל-client שהוא "רזה" יותר, יהיה לנו class אחד שיחזיק את האובייקט המלא ואחד שיחזיק את מה שעובר הלאה ל-client.

החזרת אובייקט DTO

בקוד שלנו יש את ה-class בשם Product:

קובץ product.cs

public record Product
{
    public required int id { get; init; }
    public string? name { get; init; }
    public string? description { get; init; }
    public decimal price { get; init; }
    public decimal? amount { get; init; }
    public string? producer { get; init; }
}

ואת ProductDto.

קובץ productDto.cs

public class ProductDto
{
    public required int id { get; init; }
    public string? name { get; init; }
    public string? description { get; init; }
    public decimal price { get; init; }
}

אם נסתכל על הפעולה שמביאה את כל המוצרים:

קובץ ProductsEndPoints.cs

static async Task<Ok<List<Product>>> GetAllProducts(IProductsRepository repo){
    var result = await repo.GetAllProducts();
    return TypedResults.Ok(result);
}

נראה שהיא מחזירה רשימה של Product. אנחנו רוצים ProductDto ולכן נשנה את סוג הרשימה שחוזרת.

static async Task<Ok<List<ProductDto>>> GetAllProducts(IProductsRepository repo){
    var result = await repo.GetAllProducts();
    return TypedResults.Ok(result);
}

מיפוי אובייקט

עכשיו נקבל שגיאה מכיוון שה-result שלנו לא מאותו הסוג. הוא מסוג Product. אנחנו צריכים לעשות מיפוי של האובייקטים.

static async Task<Ok<List<ProductDto>>> GetAllProducts(IProductsRepository repo){
    var result = await repo.GetAllProducts();
    var mappedResult = result.Select(x => 
        new ProductDto { 
            id = x.id, 
            description = x.description, 
            name = x.name, 
            price = x.price }).ToList();
    return TypedResults.Ok(mappedResult);
}

נסתכל על הפעולה של הוספת מוצר. אני מקבלת מה-client אובייקט קצר ורוצה למפות לאובייקט המלא. את המידע שלא מקבלים צריך להשלים עם ערכי ברירת מחדל כלשהם.

static async Task<Created<ProductDto>> AddNewProduct(ProductDto product, IProductsRepository repo) {
        var mappedResult = new Product { 
            id = product.id, 
            description = product.description, 
            name = product.name, 
            price = product.price,
            amount = 0,
        };

        Product addProduct = await repo.AddNewProduct(mappedResult);

        var productResult = new ProductDto {
            id = addProduct.id,
            description = addProduct.description,
            name = addProduct.name,
            price = addProduct.price
        };
        return TypedResults.Created($"api/products/{addProduct.id}", productResult);
}

מיפוי עם כלים אוטומטים

יש כלים שעוזרים לעשות את המיפוי, נחפש ב-nuget את automapper, automapper dependency injection.

איך עובדים איתו?

קודם מוסיפים אותו לקובץ ה-program. הוא מצפה למיקום ממנו הוא אמור להביא את הפרופילים, כלומר ההסברים של איך לבצע את המיפוי.

קובץ program.cs

builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly());

אנחנו ניצור תיקייה חדשה ונקרא לה MappingProfiles. בתוכה נייצר class בשם ProductsProfile. בתוכה אנחנו מייצרים מיפוי מ-Product ל-ProductDto וגם בכיוון ההפוך.

קובץ ProductProfile.cs

public class ProductsProfile : Profile{
    public ProductsProfile(){
        this.CreateMap<Product, ProductDto>().ReverseMap();
    }
}

עכשיו צריך לבצע את הפעולה בקובץ ה-end points. אנחנו יוצרים את mappedProduct על ידי mapper.Map כשאנחנו רוצים למפות את ה-product שקבלנו להיות מסוג Product. באותה הדרך יוצרים את productResult.

קובץ ProductsEndPoints.cs

static async Task<Created<ProductDto>> AddNewProduct(ProductDto product, 
    IProductsRepository repo, IMapper mapper) {
        var mappedProduct = mapper.Map<Product>(product);
        Product addProduct = await repo.AddNewProduct(mappedProduct);
        var productResult = mapper.Map<ProductDto>(addProduct);

        return TypedResults.Created($"api/products/{addProduct.id}", productResult);
}

גם לפעולה של הבאת המוצרים ברשימה נעבוד באותה הדרך.

static async Task<Ok<List<ProductDto>>> GetAllProducts(IProductsRepository repo,
    IMapper mapper){
    var result = await repo.GetAllProducts();
    var mappedResult = mapper.Map<List<ProductDTO>>(result);
    return TypedResults.Ok(mappedResult);
}

Custom Automapper

המיפוי שהשתמשנו בו עד עכשיו היה פשוט של שדות עם אותו שם, בכל מקום שיש התאמת שמות הוא מתאים את השדות. אם יהיה שוני בשדות, לא תהיה התאמה.

אם אנחנו צריכים לעשות התאמות, זה יתבצע בקובץ הפרופיל.

נניח שבאובייקט ה-dto שם המוצר היה בשדה productName ואליו אני רוצה להתאים את שדה name שנמצא באובייקט product.

קובץ ProductProfile.cs

public class ProductsProfile : Profile{
    public ProductsProfile(){
        this.CreateMap<Product, ProductDto>().ForMember(
            dto => dto.productName, o => o.MapFrom(p => p.name)
        ).ReverseMap();
    }
}