diff --git a/CourseLibrary.API/Controllers/AuthorsController.cs b/CourseLibrary.API/Controllers/AuthorsController.cs index a2b033b..8064c81 100644 --- a/CourseLibrary.API/Controllers/AuthorsController.cs +++ b/CourseLibrary.API/Controllers/AuthorsController.cs @@ -19,9 +19,12 @@ public class AuthorsController : ControllerBase private readonly ICourseLibraryRepository _courseLibraryRepository; private readonly IMapper _mapper; private readonly IPropertyMappingService _propertyMappingService; + private readonly IPropertyCheckerService _propertyCheckerService; public AuthorsController(ICourseLibraryRepository courseLibraryRepository, - IMapper mapper , IPropertyMappingService propertyMappingService) + IMapper mapper , IPropertyMappingService propertyMappingService, + IPropertyCheckerService propertyCheckerService + ) { _courseLibraryRepository = courseLibraryRepository ?? throw new ArgumentNullException(nameof(courseLibraryRepository)); @@ -29,11 +32,14 @@ public class AuthorsController : ControllerBase throw new ArgumentNullException(nameof(mapper)); _propertyMappingService = propertyMappingService ?? throw new ArgumentNullException(nameof(propertyMappingService)); + + _propertyCheckerService = propertyCheckerService ?? + throw new ArgumentNullException(nameof(propertyCheckerService)); } [HttpGet(Name = "GetAuthors")] [HttpHead] - public ActionResult> GetAuthors( + public IActionResult GetAuthors( [FromQuery] AuthorsResourceParameters authorsResourceParameters) { if (!_propertyMappingService.ValidMappingExistsFor @@ -42,6 +48,12 @@ public class AuthorsController : ControllerBase return BadRequest(); } + if (!_propertyCheckerService.TypeHasProperties + (authorsResourceParameters.Fields)) + { + return BadRequest(); + } + var authorsFromRepo = _courseLibraryRepository.GetAuthors(authorsResourceParameters); var previousPageLink = authorsFromRepo.HasPrevious ? @@ -65,12 +77,18 @@ public class AuthorsController : ControllerBase Response.Headers.Add("X-Pagination", JsonSerializer.Serialize(paginationMetadata)); - return Ok(_mapper.Map>(authorsFromRepo)); + return Ok(_mapper.Map>(authorsFromRepo) + .ShapeData(authorsResourceParameters.Fields)); } [HttpGet("{authorId}", Name = "GetAuthor")] - public IActionResult GetAuthor(Guid authorId) + public IActionResult GetAuthor(Guid authorId, string fields) { + if (!_propertyCheckerService.TypeHasProperties + (fields)) + { + return BadRequest(); + } var authorFromRepo = _courseLibraryRepository.GetAuthor(authorId); if (authorFromRepo == null) @@ -78,7 +96,7 @@ public IActionResult GetAuthor(Guid authorId) return NotFound(); } - return Ok(_mapper.Map(authorFromRepo)); + return Ok(_mapper.Map(authorFromRepo).ShapeData(fields)); } [HttpPost] @@ -127,6 +145,7 @@ public ActionResult DeleteAuthor(Guid authorId) return Url.Link("GetAuthors", new { + fields = authorsResourceParameters.Fields, orderBy = authorsResourceParameters.OrderBy, pageNumber = authorsResourceParameters.PageNumber - 1, pageSize = authorsResourceParameters.PageSize, @@ -137,6 +156,7 @@ public ActionResult DeleteAuthor(Guid authorId) return Url.Link("GetAuthors", new { + fields = authorsResourceParameters.Fields, orderBy = authorsResourceParameters.OrderBy, pageNumber = authorsResourceParameters.PageNumber + 1, pageSize = authorsResourceParameters.PageSize, @@ -148,6 +168,7 @@ public ActionResult DeleteAuthor(Guid authorId) return Url.Link("GetAuthors", new { + fields = authorsResourceParameters.Fields, orderBy = authorsResourceParameters.OrderBy, pageNumber = authorsResourceParameters.PageNumber, pageSize = authorsResourceParameters.PageSize, diff --git a/CourseLibrary.API/Helpers/IEnumerableExtensions.cs b/CourseLibrary.API/Helpers/IEnumerableExtensions.cs new file mode 100644 index 0000000..7073640 --- /dev/null +++ b/CourseLibrary.API/Helpers/IEnumerableExtensions.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace CourseLibrary.API.Helpers +{ + public static class IEnumerableExtensions + { + public static IEnumerable ShapeData( this IEnumerable source, string fields) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + // create a list to hold our ExpandoObjects + var expandoObjectList = new List(); + + // create a list with PropertyInfo objects on TSource. Reflection is + // expensive, so rather than doing if for each object in the list, we do + // it once and reuse the results. After all, part of the reflection is on the + // type of the object (TSource), not on the instance + var propertyInfoList = new List(); + + if (string.IsNullOrWhiteSpace(fields)) + { + // all public properties should be in the ExpandoObject + var propertyInfos = typeof(TSource) + .GetProperties(BindingFlags.Public | BindingFlags.Instance); + + propertyInfoList.AddRange(propertyInfos); + } + else + { + // the field are separated by ",", so we split it. + var fieldsAfterSplit = fields.Split(','); + + foreach (var field in fieldsAfterSplit) + { + // trim each field, as it might contain leading + // or trailing spaces. Can't trim the var in foreach, + // so use another var. + var propertyName = field.Trim(); + + // use reflection to get the property on the source object + // we need to include public and instance, b/c specifying a binding + // flag overwrites the already-existing binding flags. + var propertyInfo = typeof(TSource) + .GetProperty(propertyName, BindingFlags.IgnoreCase | + BindingFlags.Public | BindingFlags.Instance); + + if (propertyInfo == null) + { + throw new Exception($"Property {propertyName} wasn't found on" + + $" {typeof(TSource)}"); + } + + // add propertyInfo to list + propertyInfoList.Add(propertyInfo); + } + } + + // run through the source objects + foreach (TSource sourceObject in source) + { + // create an ExpandoObject that will hold the + // selected properties & values + var dataShapedObject = new ExpandoObject(); + + // Get the value of each property we have to return. For that, + // we run through the list + foreach (var propertyInfo in propertyInfoList) + { + // GetValue returns the value of the property on the source object + var propertyValue = propertyInfo.GetValue(sourceObject); + + // add the field to the ExpandoObject + ((IDictionary)dataShapedObject) + .Add(propertyInfo.Name, propertyValue); + } + + // add the ExpandoObject to the list + expandoObjectList.Add(dataShapedObject); + } + + // return the list + return expandoObjectList; + } + } +} diff --git a/CourseLibrary.API/Helpers/ObjectExtensions.cs b/CourseLibrary.API/Helpers/ObjectExtensions.cs new file mode 100644 index 0000000..4174e8d --- /dev/null +++ b/CourseLibrary.API/Helpers/ObjectExtensions.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace CourseLibrary.API.Helpers +{ + public static class ObjectExtensions + { + public static ExpandoObject ShapeData(this TSource source, + string fields) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + var dataShapedObject = new ExpandoObject(); + + if (string.IsNullOrWhiteSpace(fields)) + { + // all public properties should be in the ExpandoObject + var propertyInfos = typeof(TSource) + .GetProperties(BindingFlags.IgnoreCase | + BindingFlags.Public | BindingFlags.Instance); + + foreach (var propertyInfo in propertyInfos) + { + // get the value of the property on the source object + var propertyValue = propertyInfo.GetValue(source); + + // add the field to the ExpandoObject + ((IDictionary)dataShapedObject) + .Add(propertyInfo.Name, propertyValue); + } + + return dataShapedObject; + } + + // the field are separated by ",", so we split it. + var fieldsAfterSplit = fields.Split(','); + + foreach (var field in fieldsAfterSplit) + { + // trim each field, as it might contain leading + // or trailing spaces. Can't trim the var in foreach, + // so use another var. + var propertyName = field.Trim(); + + // use reflection to get the property on the source object + // we need to include public and instance, b/c specifying a + // binding flag overwrites the already-existing binding flags. + var propertyInfo = typeof(TSource) + .GetProperty(propertyName, + BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + + if (propertyInfo == null) + { + throw new Exception($"Property {propertyName} wasn't found " + + $"on {typeof(TSource)}"); + } + + // get the value of the property on the source object + var propertyValue = propertyInfo.GetValue(source); + + // add the field to the ExpandoObject + ((IDictionary)dataShapedObject) + .Add(propertyInfo.Name, propertyValue); + } + + // return the list + return dataShapedObject; + } + } +} diff --git a/CourseLibrary.API/ResourcesParameters/AuthorsResourceParameters.cs b/CourseLibrary.API/ResourcesParameters/AuthorsResourceParameters.cs index 9e3ede0..6e3c632 100644 --- a/CourseLibrary.API/ResourcesParameters/AuthorsResourceParameters.cs +++ b/CourseLibrary.API/ResourcesParameters/AuthorsResourceParameters.cs @@ -17,5 +17,6 @@ public class AuthorsResourceParameters set => _pageSize = (value > maxPageSize) ? maxPageSize : value; } public string OrderBy { get; set; } = "Name"; + public string Fields { get; set; } } } diff --git a/CourseLibrary.API/Services/IPropertyCheckerService.cs b/CourseLibrary.API/Services/IPropertyCheckerService.cs new file mode 100644 index 0000000..3f1fff8 --- /dev/null +++ b/CourseLibrary.API/Services/IPropertyCheckerService.cs @@ -0,0 +1,7 @@ +namespace CourseLibrary.API.Services +{ + public interface IPropertyCheckerService + { + bool TypeHasProperties(string fields); + } +} \ No newline at end of file diff --git a/CourseLibrary.API/Services/PropertyCheckerService.cs b/CourseLibrary.API/Services/PropertyCheckerService.cs new file mode 100644 index 0000000..aac1773 --- /dev/null +++ b/CourseLibrary.API/Services/PropertyCheckerService.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace CourseLibrary.API.Services +{ + public class PropertyCheckerService : IPropertyCheckerService + { + public bool TypeHasProperties(string fields) + { + if (string.IsNullOrWhiteSpace(fields)) + { + return true; + } + + // the field are separated by ",", so we split it. + var fieldsAfterSplit = fields.Split(','); + + // check if the requested fields exist on source + foreach (var field in fieldsAfterSplit) + { + // trim each field, as it might contain leading + // or trailing spaces. Can't trim the var in foreach, + // so use another var. + var propertyName = field.Trim(); + + // use reflection to check if the property can be + // found on T. + var propertyInfo = typeof(T) + .GetProperty(propertyName, + BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + + // it can't be found, return false + if (propertyInfo == null) + { + return false; + } + } + + // all checks out, return true + return true; + } + + } +} diff --git a/CourseLibrary.API/Startup.cs b/CourseLibrary.API/Startup.cs index 4644df7..49c9a57 100644 --- a/CourseLibrary.API/Startup.cs +++ b/CourseLibrary.API/Startup.cs @@ -82,6 +82,7 @@ public void ConfigureServices(IServiceCollection services) }); services.AddTransient(); + services.AddTransient(); services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); services.AddScoped();