در این مقاله، ما در مورد یک مفهوم مبتکرانه به نام data shaping و نحوه پیاده سازی آن در ASP.NET Core Web API صحبت میکنیم. برای این منظور، ما قصد داریم از ابزارهای مشابهی که در مقاله مرتب سازی در ASP.NET Core Web API استفاده کردیم استفاده کنیم. Data shaping چیزی نیست که در هر API مورد نیاز باشد، اما در بعضی موارد میتواند خیلی مفید باشد.
اگر میخواهید ما را در این مقاله همراهی کنید، میتوانید از نقطه شروع پروژه استفاده کنید و اگر میخواهید solution نهایی را بگیرید یا اگر جایی در پروژه برایتان نامفهوم است میتوانید پروژه تکمیل شده این مقاله را ببینید.
توجه: برای دنبال کردن این مقاله، کمی دانش پیشنیاز لازم است. اگر نحوه ایجاد دیتابیس یا نحوه کارکرد معماری مورد نظر را نمیدانید، به شدت توصیه میکنم که مقاله های مورد نظر را رصد نمایید.
در این مقاله ما یاد میگیریم که:
- Data Shaping چیست
- نحوه پیاده سازی Data Shaping در ASP.NET Core Web API
- تست Solution
- حل مشکلات XML Serialization
- نتیجه گیری
بحثمان را با این موضوع شروع میکنیم که data shaping دقیقا چیست.
Data Shaping چیست
Data shaping یک روش عالی برای کاهش میزان ترافیک ارسال شده از API به client است. این به استفاده کننده API، این امکان را می دهد تا داده ها را با انتخاب فیلدها از طریق query string انتخاب کند (shape کند).
منظور ما از این امر چیزی شبیه به این است:
1 |
https://localhost:5001/api/owner?fields=name,dateOfBirth |
این به API میگوید که لیستی از owner ها را فقط با فیلدهای Name
و DateOfBirth
از موجودیت owner برگرداند. در این سناریوی ما، موجودیت owner، فیلدهای خیلی زیادی برای انتخاب ندارد، اما میتوانید ببینید زمانیکه یک موجودیت، فیلدهای خیلی زیادی دارد، آنگاه این رویکرد میتواند خیلی مفید باشد. این غیر معمول نیست.
با در اختیار قراردادن این امکان برای استفاده کننده API که فقط بتواند فیلدهای مورد نیاز خود را انتخاب کند، میتوانیم به طور چشمگیری، فشار را بر روی API کاهش دهیم. از طرفی هم، این چیزی نیست که هر API به آن نیاز داشته باشد، بنابراین، ما باید خوب فکر کنیم و تصمیم بگیریم که اگر بخواهیم آن را پیاده سازی کنیم، پیاده سازی آن به کمی reflection نیاز دارد.
و ما به یقین می دانیم که reflection عوارض خودش را دارد و باعث کند شدن برنامه ما می شود.
در نهایت، مانند همیشه، data-shaping باید با مفاهیمی که تاکنون توضیح داده ایم به خوبی کار کند، یعنی با مفاهیم صفحه بندی، فیلترینگ، جست و جو و مرتب سازی.
حالا کارمان رو شروع میکنیم.
نحوه پیاده سازی Data Shaping در ASP.NET Core Web API
اول از همه، ما باید کلاس QueryStringParameters خود را توسعه دهیم ، زیرا ما می خواهیم یک ویژگی جدید به query string خود اضافه کنیم و می خواهیم این ویژگی برای هر موجودیتی در دسترس باشد:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public abstract class QueryStringParameters { const int maxPageSize = 50; public int PageNumber { get; set; } = 1; private int _pageSize = 10; public int PageSize { get { return _pageSize; } set { _pageSize = (value > maxPageSize) ? maxPageSize : value; } } public string OrderBy { get; set; } public string Fields { get; set; } } |
ما ویژگی Fields را اینجا اضافه کردیم و حالا میتوانیم از این fields، به عنوان یک پارامتر query string استفاده کنیم.
در مرحله بعدی، همان کاری که در مقاله مرتب سازی انجام دادیم ، قصد داریم اینجا نیز انجام دهیم. اما از همین ابتدا، ما آن را به صورت generic ایجاد میکنیم.
ما IDataShaper.cs و DataShaper.cs را در پوشه Helpers در پروژه Entities ایجاد میکنیم.
اول از همه، اینترفیس IDataShaper را ایجاد میکنیم:
1 2 3 4 5 |
public interface IDataShaper<T> { IEnumerable<ExpandoObject> ShapeData(IEnumerable<T> entities, string fieldsString); ExpandoObject ShapeData(T entity, string fieldsString); } |
این اینترفیس IDataShaper، دو متد را تعریف میکند که باید پیاده سازی شود. یکی برای تک موجودیت و دیگری برای کالکشنی از موجودیتها میباشد. هردوی آنها، نام ShapeData دارند، اما امضای تعریف شده در آنها متفاوت میباشد.
اینجا توجه داشته باشید که چطور از نوع ExpandoObject به عنوان نوع برگشتی استفاده میکنیم. ما باید این کار را انجام دهیم تا داده های خود را آنطور که می خواهیم shape کنیم.
و حالا، پیاده سازی واقعی را ببینیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
public class DataShaper<T> : IDataShaper<T> { public PropertyInfo[] Properties { get; set; } public DataShaper() { Properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); } public IEnumerable<ExpandoObject> ShapeData(IEnumerable<T> entities, string fieldsString) { var requiredProperties = GetRequiredProperties(fieldsString); return FetchData(entities, requiredProperties); } public ExpandoObject ShapeData(T entity, string fieldsString) { var requiredProperties = GetRequiredProperties(fieldsString); return FetchDataForEntity(entity, requiredProperties); } private IEnumerable<PropertyInfo> GetRequiredProperties(string fieldsString) { var requiredProperties = new List<PropertyInfo>(); if (!string.IsNullOrWhiteSpace(fieldsString)) { var fields = fieldsString.Split(',', StringSplitOptions.RemoveEmptyEntries); foreach (var field in fields) { var property = Properties.FirstOrDefault(pi => pi.Name.Equals(field.Trim(), StringComparison.InvariantCultureIgnoreCase)); if (property == null) continue; requiredProperties.Add(property); } } else { requiredProperties = Properties.ToList(); } return requiredProperties; } private IEnumerable<ExpandoObject> FetchData(IEnumerable<T> entities, IEnumerable<PropertyInfo> requiredProperties) { var shapedData = new List<ExpandoObject>(); foreach (var entity in entities) { var shapedObject = FetchDataForEntity(entity, requiredProperties); shapedData.Add(shapedObject); } return shapedData; } private ExpandoObject FetchDataForEntity(T entity, IEnumerable<PropertyInfo> requiredProperties) { var shapedObject = new ExpandoObject(); foreach (var property in requiredProperties) { var objectPropertyValue = property.GetValue(entity); shapedObject.TryAdd(property.Name, objectPropertyValue); } return shapedObject; } } |
حال بیاید این کلاس را به تکه های کوچک تقسیم کنیم.
پیاده سازی – مرحله به مرحله
ما در این کلاس، یک ویژگی public به نام Properties داریم. این یک آرایه ای از نوع PropertyInfo است که ما میخواهیم ویژگیهای موجود در ورودی مورد نظرمان (موجودیت) را گرفته و درون این آرایه قرار دهیم. حالا این ورودی هرچه که میخواهد باشد که در این سناریوی ما، Account یا Owner میباشد.
1 2 3 4 5 6 |
public PropertyInfo[] Properties { get; set; } public DataShaper() { Properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); } |
بنابراین در اینجا ، با نمونه کلاس مورد نظر، تمام خصوصیات یک کلاس ورودی را بدست می آوریم.
در مرحله بعد، دو متد عمومی ShapeData خودمان را پیاده سازی میکنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public IEnumerable<ExpandoObject> ShapeData(IEnumerable<T> entities, string fieldsString) { var requiredProperties = GetRequiredProperties(fieldsString); return FetchData(entities, requiredProperties); } public ExpandoObject ShapeData(T entity, string fieldsString) { var requiredProperties = GetRequiredProperties(fieldsString); return FetchDataForEntity(entity, requiredProperties); } |
این هر دو متد، شبیه به هم هستند و به متد GetRequiredProperties
متکی هستند که این متد برای تجزیه کردن رشته ورودی که شامل فیلدهایی که میخواهیم واکشی کنیم استفاده میشود.
متد GetRequiredProperties
متدی است که کارش را خیلی عالی انجام میدهد. این متد، رشته ورودی را تجزیه میکند و فقط خصوصیات موردنظری که ما نیاز داریم به کنترلر برگردانیم را برمیگرداند:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
private IEnumerable<PropertyInfo> GetRequiredProperties(string fieldsString) { var requiredProperties = new List<PropertyInfo>(); if (!string.IsNullOrWhiteSpace(fieldsString)) { var fields = fieldsString.Split(',', StringSplitOptions.RemoveEmptyEntries); foreach (var field in fields) { var property = Properties.FirstOrDefault(pi => pi.Name.Equals(field.Trim(), StringComparison.InvariantCultureIgnoreCase)); if (property == null) continue; requiredProperties.Add(property); } } else { requiredProperties = Properties.ToList(); } return requiredProperties; } |
همانطور که میبینید، چیز خاصی اینجا وجود ندارد. اگر fieldsString خالی نباشد، آن را split کرده و بررسی میکنیم که آیا فیلدها با خصوصیات موجود در موجودیت ما مطابقت دارد یا خیر. هرکدام از آنها که مطابقت داشته باشد، آن را به لیست requiredProperties اضافه میکنیم.
از سوی دیگر، اگر fieldsString خالی باشد، تمام خصوصیات درون موجودیت را به عنوان خصوصیات مورد نیاز در نظر میگیریم.
حالا متدهای FetchData
و FetchDataForEntity
، متدهای private هستند که مقادیر را از این required properties که آن را آماده کرده ایم استخراج میکند:
متد FetchDataForEntity، این کار را برای یک موجودیت انجام میدهد:
1 2 3 4 5 6 7 8 9 10 11 12 |
private ExpandoObject FetchDataForEntity(T entity, IEnumerable<PropertyInfo> requiredProperties) { var shapedObject = new ExpandoObject(); foreach (var property in requiredProperties) { var objectPropertyValue = property.GetValue(entity); shapedObject.TryAdd(property.Name, objectPropertyValue); } return shapedObject; } |
همانطور که میبینید، ما بر روی requiredProperties حلقه میزنیم و سپس کمی از reflection استفاده میکنیم. ما مقادیر را استخراج کرده و آنها را به ExpandoObject مان اضافه میکنیم. IDictionary<string,object> ،ExpandoObject را پیاده سازی میکند. بنابراین ما میتوانیم از متد TryAdd برای اضافه کردن خصوصیت مورد نظر با استفاده از name آن به عنوان کلید و value آن به عنوان مقدار برای dictionary استفاده کنیم.
با این شیوه، به طور داینامیک، ما فقط خصوصیاتی که نیاز داریم را به آبجکت داینامیکمان اضافه میکنیم.
متد FetchData همان متد FetchDataForEntity منتها برای چند آبجکت است. این متد، متد FetchDataForEntity که پیاده سازی کردیم را به کار میبرد:
1 2 3 4 5 6 7 8 9 10 11 12 |
private IEnumerable<ExpandoObject> FetchData(IEnumerable<T> entities, IEnumerable<PropertyInfo> requiredProperties) { var shapedData = new List<ExpandoObject>(); foreach (var entity in entities) { var shapedObject = FetchDataForEntity(entity, requiredProperties); shapedData.Add(shapedObject); } return shapedData; } |
هیچ چیز پیچیده ای اینجا وجود ندارد. ما فقط بر روی لیست entities حلقه زده و یک کالکشنی از shaped entities را به عنوان نتیجه برمیگردانیم.
کل پیاده سازی همین بود. حالا ببینیم که چطور ما همه اینها را به solution موجود کانکت میکنیم.
اتصال نقطه ها
حال که logic مورد نظر را پیاده سازی کردیم، میتوانیم data shaper مان را در repository ها، همانطور که با ISortHelper انجام دادیم، inject کنیم:
1 2 3 4 5 6 7 8 9 10 |
private IDataShaper<Owner> _dataShaper; public OwnerRepository(RepositoryContext repositoryContext, ISortHelper<Owner> sortHelper, IDataShaper<Owner> dataShaper) : base(repositoryContext) { _sortHelper = sortHelper; _dataShaper = dataShaper; } |
و سپس data shaping را در متد GetOwners اعمال میکنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public PagedList<ExpandoObject> GetOwners(OwnerParameters ownerParameters) { var owners = FindByCondition(o => o.DateOfBirth.Year >= ownerParameters.MinYearOfBirth && o.DateOfBirth.Year <= ownerParameters.MaxYearOfBirth); SearchByName(ref owners, ownerParameters.Name); _sortHelper.ApplySort(owners, ownerParameters.OrderBy); var shapedOwners = _dataShaper.ShapeData(owners, ownerParameters.Fields); return PagedList<ExpandoObject>.ToPagedList(shapedOwners, ownerParameters.PageNumber, ownerParameters.PageSize); } |
متد دیگری به نام GetOwnerById ایجاد کنید که داده ها را shape کند. چون ما هنوز به متد معمولیمان برای چک کردن اعتبارسنجی ها در اکشنهای کنترلر نیاز داریم:
1 2 3 4 5 6 7 8 |
public SerializableExpando GetOwnerById(Guid ownerId, string fields) { var owner = FindByCondition(owner => owner.Id.Equals(ownerId)) .DefaultIfEmpty(new Owner()) .FirstOrDefault(); return _dataShaper.ShapeData(owner, fields); } |
و فراموش نکنید که اینترفیس IOwnerRepository را برای انعکاس این تغییرات تغییر دهید:
1 2 3 4 5 6 7 8 9 |
public interface IOwnerRepository : IRepositoryBase<Owner> { PagedList<ExpandoObject> GetOwners(OwnerParameters ownerParameters); ExpandoObject GetOwnerById(Guid ownerId, string fields); Owner GetOwnerById(Guid ownerId); void CreateOwner(Owner owner); void UpdateOwner(Owner dbOwner, Owner owner); void DeleteOwner(Owner owner); } |
شما میتوانید سعی کنید AccountRepository را به عنوان تمرین خودتان تغییر دهید. اگر جایی را گیر کردید میتوانید پروژه تکمیل شده را بررسی کنید.
و البته، چون ما سازنده های کلاسهای repository را تغییر داده ایم، باید RepositoryWrapper مان را نیز تغییر دهیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
public class RepositoryWrapper : IRepositoryWrapper { private RepositoryContext _repoContext; private IOwnerRepository _owner; private IAccountRepository _account; private ISortHelper<Owner> _ownerSortHelper; private ISortHelper<Account> _accountSortHelper; private IDataShaper<Owner> _ownerDataShaper; private IDataShaper<Account> _accountDataShaper; public IOwnerRepository Owner { get { if (_owner == null) { _owner = new OwnerRepository(_repoContext, _ownerSortHelper, _ownerDataShaper); } return _owner; } } public IAccountRepository Account { get { if (_account == null) { _account = new AccountRepository(_repoContext, _accountSortHelper, _accountDataShaper); } return _account; } } public RepositoryWrapper(RepositoryContext repositoryContext, ISortHelper<Owner> ownerSortHelper, ISortHelper<Account> accountSortHelper, IDataShaper<Owner> ownerDataShaper, IDataShaper<Account> accountDataShaper) { _repoContext = repositoryContext; _ownerSortHelper = ownerSortHelper; _accountSortHelper = accountSortHelper; _ownerDataShaper = ownerDataShaper; _accountDataShaper = accountDataShaper; } public void Save() { _repoContext.SaveChanges(); } } |
استفاده از تزریق وابستگی، به این معنی است که ما باید data shaper هایمان را در متد ConfigureRepositoryWrapper کلاس ServiceExtensions
برای resolve آنها ثبت کنیم:
1 2 3 4 5 6 7 8 9 10 |
public static void ConfigureRepositoryWrapper(this IServiceCollection services) { services.AddScoped<ISortHelper<Owner>, SortHelper<Owner>>(); services.AddScoped<ISortHelper<Account>, SortHelper<Account>>(); services.AddScoped<IDataShaper<Owner>, DataShaper<Owner>>(); services.AddScoped<IDataShaper<Account>, DataShaper<Account>>(); services.AddScoped<IRepositoryWrapper, RepositoryWrapper>(); } |
و از آنجایی که ما همچین کاری را عالی انجام داده ایم، نیازی نداریم که اکشن GetOwners در کنترلر OwnerController را تغییر دهیم، اما باید یک تغییر جزئی برای بررسی validation در اکشن GetOwnerById را انجام دهیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[HttpGet("{id}", Name = "OwnerById")] public IActionResult GetOwnerById(Guid id, [FromQuery] string fields) { var owner = _repository.Owner.GetOwnerById(id, fields); if (owner == default(ExpandoObject)) { _logger.LogError($"Owner with id: {id}, hasn't been found in db."); return NotFound(); } return Ok(owner); } |
همانطور که می بینید تغییرات چندان چشمگیر نیستند.
حالا همه چیز setup شده است. ما باید این را اجرا کنیم و آن مطمئننا کار خواهد کرد. آیا همینطوره؟
در حقیقت ، اینگونه نخواهد بود.
حل مشکلات Json Serialization
اکنون ، اگر سعی کنیم برنامه را با این وضعیت اکنون اجرا کنیم ، یک InvalidCastException خیلی بد دریافت خواهیم کرد. دلیلش هم این است که System.Text از cast کردن ExpandoObject
به IDictionary
پشتیبانی نمیکند و زمانیکه نتیجه را از کنترلر برمیگردانیم این خطا چیزی است که باید اتفاق بیفتد.
برای جلوگیری از این مشکل، ما باید Microsoft.AspNetCore.Mvc.NewtonsoftJson
را از NuGet package اضافه کنیم و سپس در متد ConfigureServices در کلاس Startup.cs، خط ()services.AddControllers را پیدا کنیم و ()AddNewtonsoftJson. را به انتهای آن اضافه کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public void ConfigureServices(IServiceCollection services) { services.ConfigureCors(); services.ConfigureIISIntegration(); services.ConfigureLoggerService(); services.ConfigureMySqlContext(Configuration); services.ConfigureRepositoryWrapper(); services.AddControllers(config => { config.RespectBrowserAcceptHeader = true; config.ReturnHttpNotAcceptable = true; }).AddXmlDataContractSerializerFormatters() .AddNewtonsoftJson(); } |
کاری که این کد برای ما انجام میدهد این است که System.Text.Json
serializer پیشفرض را با Newtonsoft.Json جایگزین میکند. حالا دیگر exception باید رفع شده باشد و برنامه به درستی کار کند.
از آنجایی که ما در این مرحله از کار هستیم، قصد داریم برنامه را طوری پیکربندی کنیم که بر روی browser header ها حساس باشد و اگر یک media type ناشناخته مورد درخواست قرار بگیرد، 406 Not Acceptable به عنوان پاسخ برگشت داده شود و از یک XML serializer به عنوان قرارداد برای داده ها استفاده کند زیرا ما میخواهیم از نقل و انتقال محتوا پشتیبانی کنیم.
این، مشکل را با json serialization حل میکند.
اما بیایید solution خود را تست کنیم تا ببینیم آیا کاری را که انجام داده ایم آیا واقعا کار میکند یا خیر.
تست Solution
قبل از اینکه solution را تست کنیم، یک نگاه سریع به جدول Owner بندازیم و ببینیم که چطور به نظر میرسد:

عالیه. این ورودی ها باید از عهده تست ما بربیایند.
ابتدا اجازه دهید یک درخواست ساده GET به owners endpoint خود ارسال کنیم:
1 |
https://localhost:5001/api/owner |
در پاسخ باید یک full response را دریافت کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
[ { "Id": "261e1685-cf26-494c-b17c-3546e65f5620", "Name": "Anna Bosh", "DateOfBirth": "1974-11-14T00:00:00", "Address": "27 Colored Row" }, { "Id": "9c362f85-5581-4182-ac96-b7b88a74dda7", "Name": "Anna Bosh", "DateOfBirth": "1964-11-14T00:00:00", "Address": "24 Crescent Street" }, { "Id": "24fd81f8-d58a-4bcc-9f35-dc6cd5641906", "Name": "John Keen", "DateOfBirth": "1980-12-05T00:00:00", "Address": "61 Wellfield Road" }, { "Id": "f98e4d74-0f68-4aac-89fd-047f1aaca6b6", "Name": "Martin Miller", "DateOfBirth": "1983-05-21T00:00:00", "Address": "3 Edgar Buildings" }, { "Id": "66774006-2371-4d5b-8518-2177bcf3f73e", "Name": "Nick Somion", "DateOfBirth": "1998-12-15T00:00:00", "Address": "North sunny address 102" }, { "Id": "a3c1880c-674c-4d18-8f91-5d3608a2c937", "Name": "Sam Query", "DateOfBirth": "1990-04-22T00:00:00", "Address": "91 Western Roads" } ] |
بنابراین عملکرد data shaping ما، اختلالی در رفتار پیشفرض برنامه ایجاد نکرده است.
حالا data shaping را در عمل بینیم:
1 |
https://localhost:5001/api/owner?fields=name,dateOfBirth |
API باید اینجا، داده های shape شده را برگرداند:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
[ { "Name": "Sam Query", "DateOfBirth": "1990-04-22T00:00:00" }, { "Name": "Nick Somion", "DateOfBirth": "1998-12-15T00:00:00" }, { "Name": "Martin Miller", "DateOfBirth": "1983-05-21T00:00:00" }, { "Name": "John Keen", "DateOfBirth": "1980-12-05T00:00:00" }, { "Name": "Anna Bosh", "DateOfBirth": "1974-11-14T00:00:00" }, { "Name": "Anna Bosh", "DateOfBirth": "1964-11-14T00:00:00" } ] |
و اکنون برای تکمیل آن ، بیایید ببینیم آیا این با صفحه بندی ، فیلترینگ، جستجو و مرتب سازی کار می کند یا خیر:
1 |
https://localhost:5001/api/owner?fields=name,dateOfBirth&pageSize=2&pageNumber=1&orderBy=name asc,dateOfBirth desc&maxYearOfBirth=1970 |
این کوئری، فقط باید یک نتیجه را برگرداند. آیا میتوانید حدس بزنید که کدام یک از owner ها را به عنوان نتیجه برمیگرداند؟ اگر حدس زدید نتیجه درست چیست آن را در قسمت نظرات برای ما بنویسید.
تمام. ما API مان را با موفقیت تست کردیم.
یک چیز دیگر را نیز تست کنیم. بیایید سعی کنیم یک نتیجه XML را درخواست کنیم.
حل مشکلات XML Serialization
Accept header را به application/xml تغییر میدهیم و یک درخواست را میفرستیم. میخواهیم خروجی را تست کنیم و ببینیم که آیا نقل و انتقال محتوا کار میکند یا خیر.
اینبار قصد داریم یک درخواست ساده بفرستیم:
1 |
https://localhost:5001/api/owner/a3c1880c-674c-4d18-8f91-5d3608a2c937 |
و response دریافتی به این صورت میباشد:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<ArrayOfKeyValueOfstringanyType xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/2003/10/Serialization/Arrays"> <KeyValueOfstringanyType> <Key>Id</Key> <Value xmlns:d3p1="http://schemas.microsoft.com/2003/10/Serialization/" i:type="d3p1:guid">a3c1880c-674c-4d18-8f91-5d3608a2c937</Value> </KeyValueOfstringanyType> <KeyValueOfstringanyType> <Key>Name</Key> <Value xmlns:d3p1="http://www.w3.org/2001/XMLSchema" i:type="d3p1:string">Sam Query</Value> </KeyValueOfstringanyType> <KeyValueOfstringanyType> <Key>DateOfBirth</Key> <Value xmlns:d3p1="http://www.w3.org/2001/XMLSchema" i:type="d3p1:dateTime">1990-04-22T00:00:00</Value> </KeyValueOfstringanyType> <KeyValueOfstringanyType> <Key>Address</Key> <Value xmlns:d3p1="http://www.w3.org/2001/XMLSchema" i:type="d3p1:string">91 Western Roads</Value> </KeyValueOfstringanyType> </ArrayOfKeyValueOfstringanyType> |
همانطور که میبینید این response بسیار بد و غیرقابل خوانا میباشد. اما این نشان میدهد که XmlDataContractSerializerOutputFormatter چطور ExpandoObject ما را به صورت پیشفرض serialize میکند.
بنابراین آیا می خواهیم این مسئله را برطرف کنیم و اگر اینطور است چطور باید آن را انجام دهیم؟
این خودش یک مقاله جداگانه میباشد که خارج از حوزه این مقاله است، اما شما میتوانید solution را در پروژه تکمیل شده این مقاله مشاهده کنید.
یک توضیح ساده این است که ما باید آبجکت dynamic خود را ایجاد کنیم و قوانین XML serialization را برای آن تعریف کنیم.
بنابراین ما باید چیزی همانند این را ایجاد کنیم:
1 2 3 4 5 6 |
public class Entity : DynamicObject, IXmlSerializable, IDictionary<string, object> { //... //implementation //... } |
مهمترین نکته ای که اینجا باید به آن توجه کنید این است که ما از DynamicObject ارث بری کرده ایم که آبجکت ما را از نوع dynamic ایجاد میکند. همچنین از اینترفیس IXmlSerializable ارث بری کرده ایم که ما باید قوانین serialization سفارشی را پیاده سازی کنیم و همچنین به دلیل متد Add که برای XML serialization مورد نیاز است از IDictionary<string, object> ارث بری کرده ایم.
تنها چیزی که باقیمانده است این است که نوع ExpandoObject را با نوع Entity در کل پروژه جایگزین کنیم.
حالا باید یک response به مانند زیر دریافت کنیم:
1 2 3 4 5 6 |
<Entity xmlns="http://schemas.datacontract.org/2004/07/Entities.Models"> <Id>a3c1880c-674c-4d18-8f91-5d3608a2c937</Id> <Name>Sam Query</Name> <DateOfBirth>4/22/1990 12:00:00 AM</DateOfBirth> <Address>91 Western Roads</Address> </Entity> |
حالا این خیلی بهتر است. آیا اینطور نیست؟
اگر XML serialization برای شما مهم نیست، میتوانید از همان ExpandoObject استفاده کنید. اما اگر یک XML response را بخواهید که به خوبی فرمت بندی شده باشد، این راه حلش میباشد.
خب، حالا تمام کاری که تا الان انجام داده ایم را خلاصه میکنیم.
نتیجه گیری
Data shaping یک ویژگی کوچک جذاب و شگفت انگیز است که واقعاً می تواند API های ما را انعطاف پذیر کرده و از ترافیک شبکه ما بکاهد. اگر ما دارای یک API با ترافیک حجم بالا هستیم ، Data shaping باید به خوبی برایمان کار کند. از طرف دیگر، این ویژگی نیست که ما به راحتی از آن استفاده کنیم، زیرا آن برای انجام کارها از reflection و dynamic type استفاده می کند.
مانند سایر ویژگیهای دیگر، ما باید حواسمان باشد که چه زمانی و اینکه آیا اصلا باید Data shaping را پیاده سازی کنیم یا خیر. حتی اگر آن را پیاده سازی کنیم هم تست های عملکرد ممکن است مفید باشد.
در این مقاله، ما یاد گرفتیم که:
- data shaping چیست
- نحوه پیاده سازی یک data shaping solution به صورت generic در ASP.NET Core Web API به چه صورت است
- نحوه حل مشکلات json serialization با ExpandoObject چگونه است
- تست solution با ارسال تعدادی درخواست ساده
- قالب بندی XML response هایمان
اگر جایی از این مقاله برایتان نامفهموم بود، پیشنهاد میکنیم که یک نگاه سریع به دیگر قسمتهای این سری از آموزش Asp.net Core بیندازید: صفحه بندی، فیلترینگ، جست و جو و مرتب سازی. خصوصا مقاله صفحه بندی خیلی مهم است، زیرا ما زیرساخت مورد نیاز برای کل این سری از آموزش را در آن مقاله، تنظیم کرده ایم.
امیدوارم در این مقاله، چیز جدید و جالبی را یاد گرفته باشید. در مقاله بعدی، ما میخواهیم نحوه پیاده سازی HATEOAS را پوشش دهیم.