Reputation: 175
This question is mostly not a problem help call, but an invitation for the comparison between developers on contemporary, more advanced and clean development methods.
Here I explain how I've resolved this problem and show you a working code example. Anyway, I have some doubts about my solution. I'm really curios if into nature exists a more elegant way to achieve the same code optimization.
I've started from the issue when I had two different controllers with mostly same response models except Items property type.
Precision: We need to have dedicated and not shared response models for each controller. In my opinion it helps in the future, when the only one controller's response have to be changed without creating side effects for the others.
I've started from the problem when I had two different controllers with mostly same response models except Items property type.
Here them are:
namespace Webapi.Models.File {
public class Response {
public FileItem [] Items { get; set; }
public int Page { get; set; }
public int TotalPages { get; set; }
}
public class FileItem {
...
}
}
namespace Webapi.Models.User {
public class Response {
public UserItem [] Items { get; set; }
public int Page { get; set; }
public int TotalPages { get; set; }
}
public class UserItem {
...
}
}
The first model is populated in this way:
using FileModel = Webapi.Models.File;
private FileModel.Response CreateItemsPage(List<FileModel.FileItem> items, int page) {
int maxItemsPerPage = 50;
var chunks = items.Select((v, i) => new { Value = v, Index = i })
.GroupBy(x => x.Index / maxItemsPerPage).Select(grp => grp.Select(x => x.Value));
int totalChunks = chunks.Count();
if(totalChunks == 0) {
return null;
}
page = page > 1 ? page : 1;
page = totalChunks < page ? 1 : page;
return new FileModel.Response() {
Items = (chunks.ToArray())[page-1].ToArray(),
Page = page,
TotalPages = totalChunks
};
}
And the second method is exactly the same except input (List<UserModel.UserItem>) and output (UserModel.Response) types:
using UserModel = Webapi.Models.User;
private UserModel.Response CreateItemsPage(List<UserModel.UserItem> items, int page) {
int maxItemsPerPage = 50;
var chunks = items.Select((v, i) => new { Value = v, Index = i })
.GroupBy(x => x.Index / maxItemsPerPage).Select(grp => grp.Select(x => x.Value));
int totalChunks = chunks.Count();
if(totalChunks == 0) {
return null;
}
page = page > 1 ? page : 1;
page = totalChunks < page ? 1 : page;
return new UserModel.Response() {
Items = (chunks.ToArray())[page-1].ToArray(),
Page = page,
TotalPages = totalChunks
};
}
Having two and then even more cloned methods inside my webapi's controllers isn't a good perspective and I have resolved this by creating two ObjectExtensions methods.
The first one just reassign properties from source to target object. Both logically must have same properties inside (name and type):
public static TTarget AssignProperties<TTarget, TSource>(this TTarget target, TSource source) {
foreach (var targetProp in target.GetType().GetProperties()) {
foreach (var sourceProp in source.GetType().GetProperties()) {
if (targetProp.Name == sourceProp.Name && targetProp.PropertyType == sourceProp.PropertyType) {
targetProp.SetValue(target, sourceProp.GetValue(source));
break;
}
}
}
return target;
}
The second receives target and source objects, creates internally an anonymous one, then reassigns properties from it by using the previous extension method AssignProperties
to the target object (need this because cannot access generic objects properties directly):
public static TTarget CreateItemsPage<TTarget, TSource>(this TTarget target, List<TSource> items, int page = 1) {
int maxItemsPerPage = 50;
var chunks = items.Select((v, i) => new { Value = v, Index = i })
.GroupBy(x => x.Index / maxItemsPerPage).Select(grp => grp.Select(x => x.Value));
int totalChunks = chunks.Count();
if(totalChunks == 0) {
return target;
}
page = page > 1 ? page : 1;
page = totalChunks < page ? 1 : page;
var source = new {
Items = (chunks.ToArray())[page-1].ToArray(),
Page = page,
TotalPages = totalChunks
};
target = target.AssignProperties(source);
return target;
}
And here is the usage:
...
var items = _filesService.ListAllUserFiles(userId, requestData.SearchText);
if(pages.Count() == 0)
return BadRequest();
return Ok(new FileModel.Response().CreateItemsPage(items, requestData.Page));
...
Some code examples would be appreciated. Thank you!
Upvotes: 0
Views: 398
Reputation: 175
By opening a discussion on Reddit I came to the following solution that allows me to keep models separate and remove the inefficient AssignProperties method.
Interface:
public interface IPaginationResponse<TItem> {
TItem[] Items { get; set; }
int Page { get; set; }
int TotalPages { get; set; }
}
Models examples:
public class Response: IPaginationResponse<Info> {
public Info [] Items { get; set; }
public int Page { get; set; }
public int TotalPages { get; set; }
...
}
public class Response: IPaginationResponse<UserFile> {
public UserFile [] Items { get; set; }
public int Page { get; set; }
public int TotalPages { get; set; }
...
}
public class Response: IPaginationResponse<UserItem> {
public UserItem [] Items { get; set; }
public int Page { get; set; }
public int TotalPages { get; set; }
...
}
Now finally I've removed AssignProperties
from CreateItemsPage
extension method. Thanks to where TTarget : IPaginationResponse<TSource>
I can assign values directly to TTarget target
public static TTarget CreateItemsPage<TTarget, TSource>(this TTarget target, IEnumerable<TSource> items, int page = 1) where TTarget : IPaginationResponse<TSource> {
...
target.Items = (chunks.ToArray())[page-1].ToArray();
target.Page = page;
target.TotalPages = totalChunks;
return target;
}
Inside controller I invoke it in the same way
return Ok(new FileModel.Response().CreateItemsPage(pages, requestData.Page));
Upvotes: 1
Reputation: 8962
As both Response
classes are different in Item
type only, a single generic type could be used instead of two non-generic types.
public class ResponsePageTemplate<TItem>
{
public TItem[] Items { get; set; }
public int Page { get; set; }
public int TotalPages { get; set; }
}
Then extension method would be like:
public static ResponsePageTemplate<TSource> CreateItemsPage<TSource>(this IEnumerable<TSource> items, int page = 1)
{
int maxItemsPerPage = 50;
var chunks = items.Select((v, i) => new {Value = v, Index = i})
.GroupBy(x => x.Index / maxItemsPerPage).Select(grp => grp.Select(x => x.Value));
int totalChunks = chunks.Count();
if (totalChunks == 0)
{
return new ResponsePageTemplate<TSource>();
}
page = page > 1 ? page : 1;
page = totalChunks < page ? 1 : page;
var result = new ResponsePageTemplate<TSource>
{
Items = (chunks.ToArray())[page - 1].ToArray(),
Page = page,
TotalPages = totalChunks
};
return result;
}
And usage would be like:
return Ok(items.CreateItemsPage(requestData.Page));
Upvotes: 0