From 96a65708f4a1726517ef9ff1c176b45ce0d01249 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 29 Aug 2016 14:31:22 -0500 Subject: [PATCH 01/28] add overridable controllers --- .../IJsonApiModelConfiguration.cs | 30 +++++- .../JsonApiConfigurationBuilder.cs | 95 +++++++++++++++++++ .../JsonApiModelConfiguration.cs | 54 ++++------- .../Controllers/ControllerBuilder.cs | 34 ++++++- .../Controllers/IJsonApiController.cs | 15 +++ .../Controllers/JsonApiController.cs | 14 +-- .../IServiceCollectionExtensions.cs | 12 +-- JsonApiDotNetCore/Routing/Router.cs | 2 +- .../Controllers/TodoItemsController.cs | 24 +++++ .../Controllers/ValuesController.cs | 44 --------- JsonApiDotNetCoreExample/Startup.cs | 7 +- JsonApiDotNetCoreExample/appsettings.json | 2 +- README.md | 71 +++++++++++++- 13 files changed, 292 insertions(+), 112 deletions(-) create mode 100644 JsonApiDotNetCore/Configuration/JsonApiConfigurationBuilder.cs create mode 100644 JsonApiDotNetCore/Controllers/IJsonApiController.cs create mode 100644 JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs delete mode 100644 JsonApiDotNetCoreExample/Controllers/ValuesController.cs diff --git a/JsonApiDotNetCore/Configuration/IJsonApiModelConfiguration.cs b/JsonApiDotNetCore/Configuration/IJsonApiModelConfiguration.cs index 583a5f359d..7b4ba67c19 100644 --- a/JsonApiDotNetCore/Configuration/IJsonApiModelConfiguration.cs +++ b/JsonApiDotNetCore/Configuration/IJsonApiModelConfiguration.cs @@ -1,13 +1,39 @@ using System; using System.Collections.Generic; -using AutoMapper; +using JsonApiDotNetCore.Abstractions; namespace JsonApiDotNetCore.Configuration { public interface IJsonApiModelConfiguration { + /// + /// The database context to use + /// + /// + /// void UseContext(); + + /// + /// The request namespace. + /// + /// + /// api/v1 void SetDefaultNamespace(string ns); - void DefineResourceMapping(Action> mapping); + + /// + /// Define explicit mapping of a model to a class that implements IJsonApiResource + /// + /// + /// + /// + void AddResourceMapping(Type modelType, Type resourceType); + + /// + /// Specifies a controller override class for a particular model type. + /// + /// + /// + /// + void UseController(Type modelType, Type controllerType); } } diff --git a/JsonApiDotNetCore/Configuration/JsonApiConfigurationBuilder.cs b/JsonApiDotNetCore/Configuration/JsonApiConfigurationBuilder.cs new file mode 100644 index 0000000000..0375e75b11 --- /dev/null +++ b/JsonApiDotNetCore/Configuration/JsonApiConfigurationBuilder.cs @@ -0,0 +1,95 @@ +using System; +using System.Reflection; +using JsonApiDotNetCore.Routing; +using Microsoft.EntityFrameworkCore; +using System.Linq; +using AutoMapper; +using JsonApiDotNetCore.Abstractions; +using JsonApiDotNetCore.Attributes; + +namespace JsonApiDotNetCore.Configuration +{ + public class JsonApiConfigurationBuilder + { + private readonly Action _configurationAction; + private JsonApiModelConfiguration Config { get; set; } + + public JsonApiConfigurationBuilder(Action configurationAction) + { + Config = new JsonApiModelConfiguration(); + _configurationAction = configurationAction; + } + + public JsonApiModelConfiguration Build() + { + Config = new JsonApiModelConfiguration(); + _configurationAction.Invoke(Config); + CheckIsValidConfiguration(); + LoadModelRoutesFromContext(); + SetupResourceMaps(); + return Config; + } + + private void CheckIsValidConfiguration() + { + if (Config.ContextType == null) + throw new NullReferenceException("DbContext is not specified"); + } + + private void LoadModelRoutesFromContext() + { + // Assumption: all DbSet<> types should be included in the route list + var properties = Config.ContextType.GetProperties().ToList(); + + properties.ForEach(property => + { + if (property.PropertyType.GetTypeInfo().IsGenericType && + property.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)) + { + + var modelType = property.PropertyType.GetGenericArguments()[0]; + + var route = new RouteDefinition + { + ModelType = modelType, + PathString = RouteBuilder.BuildRoute(Config.Namespace, property.Name), + ContextPropertyName = property.Name + }; + + Config.Routes.Add(route); + } + }); + } + + private void SetupResourceMaps() + { + LoadDefaultResourceMaps(); + var mapConfiguration = new MapperConfiguration(cfg => + { + foreach (var definition in Config.ResourceMapDefinitions) + { + cfg.CreateMap(definition.Key, definition.Value); + } + }); + + Config.ResourceMapper = mapConfiguration.CreateMapper(); + } + + private void LoadDefaultResourceMaps() + { + var resourceAttribute = typeof(JsonApiResourceAttribute); + var modelTypes = Assembly.GetEntryAssembly().DefinedTypes.Where(t => t.GetCustomAttributes(resourceAttribute).Count() == 1); + + foreach (var modelType in modelTypes) + { + var resourceType = ((JsonApiResourceAttribute)modelType.GetCustomAttribute(resourceAttribute)).JsonApiResourceType; + + // do not overwrite custom definitions + if(!Config.ResourceMapDefinitions.ContainsKey(modelType.UnderlyingSystemType)) + { + Config.ResourceMapDefinitions.Add(modelType.UnderlyingSystemType, resourceType); + } + } + } + } +} diff --git a/JsonApiDotNetCore/Configuration/JsonApiModelConfiguration.cs b/JsonApiDotNetCore/Configuration/JsonApiModelConfiguration.cs index e4c194908b..7c4fe2611a 100644 --- a/JsonApiDotNetCore/Configuration/JsonApiModelConfiguration.cs +++ b/JsonApiDotNetCore/Configuration/JsonApiModelConfiguration.cs @@ -3,6 +3,8 @@ using System.Linq; using System.Reflection; using AutoMapper; +using JsonApiDotNetCore.Abstractions; +using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.JsonApi; using JsonApiDotNetCore.Routing; using Microsoft.AspNetCore.Http; @@ -17,58 +19,36 @@ public class JsonApiModelConfiguration : IJsonApiModelConfiguration public Type ContextType { get; set; } public List Routes = new List(); public Dictionary ResourceMapDefinitions = new Dictionary(); + public Dictionary ControllerOverrides = new Dictionary(); public void SetDefaultNamespace(string ns) { Namespace = ns; } - public void DefineResourceMapping(Action> mapping) + // TODO: change to AddResourceMapping(Type, Type) + public void AddResourceMapping(Type modelType, Type resourceType) { - mapping.Invoke(ResourceMapDefinitions); + if (!resourceType.GetInterfaces().Contains(typeof(IJsonApiResource))) + throw new ArgumentException("Specified type does not implement IJsonApiResource", nameof(resourceType)); - var mapConfiguration = new MapperConfiguration(cfg => - { - foreach (var definition in ResourceMapDefinitions) - { - cfg.CreateMap(definition.Key, definition.Value); - } - }); - - ResourceMapper = mapConfiguration.CreateMapper(); + ResourceMapDefinitions.Add(modelType, resourceType); } - public void UseContext() + public void UseController(Type modelType, Type controllerType) { - // TODO: assert the context is of type DbContext - ContextType = typeof(T); - LoadModelRoutesFromContext(); + if(!controllerType.GetInterfaces().Contains(typeof(IJsonApiController))) + throw new ArgumentException("Specified type does not implement IJsonApiController", nameof(controllerType)); + + ControllerOverrides[modelType] = controllerType; } - private void LoadModelRoutesFromContext() + public void UseContext() { - // Assumption: all DbSet<> types should be included in the route list - var properties = ContextType.GetProperties().ToList(); - - properties.ForEach(property => - { - if (property.PropertyType.GetTypeInfo().IsGenericType && - property.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)) - { - - var modelType = property.PropertyType.GetGenericArguments()[0]; - - var route = new RouteDefinition - { - ModelType = modelType, - PathString = RouteBuilder.BuildRoute(Namespace, property.Name), - ContextPropertyName = property.Name - }; + ContextType = typeof(T); - Routes.Add(route); - } - }); + if (!typeof(DbContext).IsAssignableFrom(ContextType)) + throw new ArgumentException("Context Type must derive from DbContext", nameof(T)); } - } } diff --git a/JsonApiDotNetCore/Controllers/ControllerBuilder.cs b/JsonApiDotNetCore/Controllers/ControllerBuilder.cs index c3bdf1da44..ae556c6efc 100644 --- a/JsonApiDotNetCore/Controllers/ControllerBuilder.cs +++ b/JsonApiDotNetCore/Controllers/ControllerBuilder.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using JsonApiDotNetCore.Abstractions; using JsonApiDotNetCore.Data; @@ -13,9 +16,36 @@ public ControllerBuilder(JsonApiContext context) _context = context; } - public JsonApiController BuildController() + public IJsonApiController BuildController() { - return new JsonApiController(_context, new ResourceRepository(_context)); + var overrideController = GetOverrideController(); + return overrideController ?? new JsonApiController(_context, new ResourceRepository(_context)); + } + + public IJsonApiController GetOverrideController() + { + Type controllerType; + return _context.Configuration.ControllerOverrides.TryGetValue(_context.GetEntityType(), out controllerType) ? + InstantiateController(controllerType) : null; + } + + private IJsonApiController InstantiateController(Type controllerType) + { + var constructor = controllerType.GetConstructors()[0]; + var parameters = constructor.GetParameters(); + var services = + parameters.Select(param => GetService(param.ParameterType)).ToArray(); + return (IJsonApiController) Activator.CreateInstance(controllerType, services); + } + + private object GetService(Type serviceType) + { + if(serviceType == typeof(ResourceRepository)) + return new ResourceRepository(_context); + if (serviceType == typeof(JsonApiContext)) + return _context; + + return _context.HttpContext.RequestServices.GetService(serviceType); } } } diff --git a/JsonApiDotNetCore/Controllers/IJsonApiController.cs b/JsonApiDotNetCore/Controllers/IJsonApiController.cs new file mode 100644 index 0000000000..92b196c37a --- /dev/null +++ b/JsonApiDotNetCore/Controllers/IJsonApiController.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; + +namespace JsonApiDotNetCore.Controllers +{ + public interface IJsonApiController + { + ObjectResult Delete(string id); + ObjectResult Get(); + ObjectResult Get(string id); + ObjectResult Patch(string id, Dictionary entityPatch); + ObjectResult Post(object entity); + } +} \ No newline at end of file diff --git a/JsonApiDotNetCore/Controllers/JsonApiController.cs b/JsonApiDotNetCore/Controllers/JsonApiController.cs index 86a21d695f..015963cae8 100644 --- a/JsonApiDotNetCore/Controllers/JsonApiController.cs +++ b/JsonApiDotNetCore/Controllers/JsonApiController.cs @@ -6,7 +6,7 @@ namespace JsonApiDotNetCore.Controllers { - public class JsonApiController + public class JsonApiController : IJsonApiController { protected readonly JsonApiContext JsonApiContext; private readonly ResourceRepository _resourceRepository; @@ -17,16 +17,16 @@ public JsonApiController(JsonApiContext jsonApiContext, ResourceRepository resou _resourceRepository = resourceRepository; } - public ObjectResult Get() + public virtual ObjectResult Get() { var entities = _resourceRepository.Get(); - if(entities == null || entities.Count == 0) { + if(entities == null) { return new NotFoundObjectResult(null); } return new OkObjectResult(entities); } - public ObjectResult Get(string id) + public virtual ObjectResult Get(string id) { var entity = _resourceRepository.Get(id); if(entity == null) { @@ -35,14 +35,14 @@ public ObjectResult Get(string id) return new OkObjectResult(entity); } - public ObjectResult Post(object entity) + public virtual ObjectResult Post(object entity) { _resourceRepository.Add(entity); _resourceRepository.SaveChanges(); return new CreatedResult(JsonApiContext.HttpContext.Request.Path, entity); } - public ObjectResult Patch(string id, Dictionary entityPatch) + public virtual ObjectResult Patch(string id, Dictionary entityPatch) { var entity = _resourceRepository.Get(id); if(entity == null) { @@ -55,7 +55,7 @@ public ObjectResult Patch(string id, Dictionary entityPatc return new OkObjectResult(entity); } - public ObjectResult Delete(string id) + public virtual ObjectResult Delete(string id) { _resourceRepository.Delete(id); _resourceRepository.SaveChanges(); diff --git a/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 46e16e7757..1477f16410 100644 --- a/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -10,14 +10,10 @@ public static class IServiceCollectionExtensions { public static void AddJsonApi(this IServiceCollection services, Action configurationAction) { - var config = new JsonApiModelConfiguration(); - configurationAction.Invoke(config); - - if (config.ResourceMapper == null) - { - config.ResourceMapper = new MapperConfiguration(cfg => {}).CreateMapper(); - } - services.AddSingleton(_ => new Router(config)); + var configBuilder = new JsonApiConfigurationBuilder(configurationAction); + var config = configBuilder.Build(); + IRouter router = new Router(config); + services.AddSingleton(_ => router); } } } diff --git a/JsonApiDotNetCore/Routing/Router.cs b/JsonApiDotNetCore/Routing/Router.cs index e0a2e485c7..8cdb0df808 100644 --- a/JsonApiDotNetCore/Routing/Router.cs +++ b/JsonApiDotNetCore/Routing/Router.cs @@ -48,7 +48,7 @@ private void CallController() SendResponse(result); } - private ObjectResult ActivateControllerMethod(JsonApiController controller) + private ObjectResult ActivateControllerMethod(IJsonApiController controller) { var route = _jsonApiContext.Route; switch (route.RequestMethod) diff --git a/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs b/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs new file mode 100644 index 0000000000..e838d55dc8 --- /dev/null +++ b/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs @@ -0,0 +1,24 @@ +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Abstractions; +using JsonApiDotNetCore.Data; +using JsonApiDotNetCoreExample.Data; +using Microsoft.AspNetCore.Mvc; +using System.Linq; + +namespace JsonApiDotNetCoreExample.Controllers +{ + public class TodoItemsController : JsonApiController, IJsonApiController + { + private readonly ApplicationDbContext _dbContext; + + public TodoItemsController(JsonApiContext jsonApiContext, ResourceRepository resourceRepository, ApplicationDbContext applicationDbContext) : base(jsonApiContext, resourceRepository) + { + _dbContext = applicationDbContext; + } + + public override ObjectResult Get() + { + return new OkObjectResult(_dbContext.TodoItems.ToList()); + } + } +} diff --git a/JsonApiDotNetCoreExample/Controllers/ValuesController.cs b/JsonApiDotNetCoreExample/Controllers/ValuesController.cs deleted file mode 100644 index cf076106ae..0000000000 --- a/JsonApiDotNetCoreExample/Controllers/ValuesController.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -namespace JsonApiDotNetCoreExample.Controllers -{ - [Route("api/[controller]")] - public class ValuesController : Controller - { - // GET api/values - [HttpGet] - public IEnumerable Get() - { - return new string[] { "value1", "value2" }; - } - - // GET api/values/5 - [HttpGet("{id}")] - public string Get(int id) - { - return "value"; - } - - // POST api/values - [HttpPost] - public void Post([FromBody]string value) - { - } - - // PUT api/values/5 - [HttpPut("{id}")] - public void Put(int id, [FromBody]string value) - { - } - - // DELETE api/values/5 - [HttpDelete("{id}")] - public void Delete(int id) - { - } - } -} diff --git a/JsonApiDotNetCoreExample/Startup.cs b/JsonApiDotNetCoreExample/Startup.cs index 4d5fb5d104..638f323e06 100644 --- a/JsonApiDotNetCoreExample/Startup.cs +++ b/JsonApiDotNetCoreExample/Startup.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCoreExample.Controllers; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCoreExample.Resources; @@ -39,11 +40,7 @@ public void ConfigureServices(IServiceCollection services) services.AddJsonApi(config => { config.SetDefaultNamespace("api/v1"); config.UseContext(); - config.DefineResourceMapping(dictionary => - { - dictionary.Add(typeof(TodoItem), typeof(TodoItemResource)); - dictionary.Add(typeof(Person), typeof(PersonResource)); - }); + config.UseController(typeof(TodoItem), typeof(TodoItemsController)); }); } diff --git a/JsonApiDotNetCoreExample/appsettings.json b/JsonApiDotNetCoreExample/appsettings.json index 85282572fa..02bbe22262 100755 --- a/JsonApiDotNetCoreExample/appsettings.json +++ b/JsonApiDotNetCoreExample/appsettings.json @@ -1,6 +1,6 @@ { "Data": { - "ConnectionString": "User ID=dotnet;Host=localhost;Port=5432;Database=JsonApiDotNetCoreExample;Pooling=true;" + "ConnectionString": "User ID=postgres;Password=postgres;Host=localhost;Port=5432;Database=JsonApiDotNetCoreExample;Pooling=true;" }, "Logging": { "IncludeScopes": false, diff --git a/README.md b/README.md index 0c3c41d2b0..78f81fbccc 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,6 @@ services.AddDbContext(options => services.AddJsonApi(config => { config.SetDefaultNamespace("api/v1"); config.UseContext(); - config.DefineResourceMapping(dictionary => - { - dictionary.Add(typeof(TodoItem), typeof(TodoItemResource)); - dictionary.Add(typeof(Person), typeof(PersonResource)); - }); }); ``` @@ -30,6 +25,72 @@ services.AddJsonApi(config => { app.UseJsonApi(); ``` +## Specifying The Presenter / ViewModel + +When you define a model, you **must** specify the associated `JsonApiResource` class which **must** implement `IJsonApiResource`. +This is used for mapping out only the data that should be available to client applications. + +For example: + +``` +[JsonApiResource(typeof(PersonResource))] +public class Person +{ + public int Id { get; set; } + public string Name { get; set; } + public string SomethingSecret { get; set; } + public virtual List TodoItems { get; set; } +} + +public class PersonResource : IJsonApiResource +{ + public string Id { get; set; } + public string Name { get; set; } +} +``` + +We use [AutoMapper](http://automapper.org/) as the mapping tool. +You can specify a custom mapping configuration in your `Startup` class like so: + +``` +// not implemented +``` + +## Overriding controllers + +You can define your own controllers that implement the `IJsonApiController` like so: + +``` +services.AddJsonApi(config => { + ... + config.UseController(typeof(TodoItem), typeof(TodoItemsController)); + ... +}); +``` + +The controller **must** implement `IJsonApiController`, and it **may** inherit from `JsonApiController`. +Constructor dependency injection will work like normal. +Any services added in your `Startup.ConfigureServices()` method will be injected into the constructor parameters. + +``` +public class TodoItemsController : JsonApiController, IJsonApiController +{ + private ApplicationDbContext _dbContext; + + public TodoItemsController(JsonApiContext jsonApiContext, ResourceRepository resourceRepository, ApplicationDbContext applicationDbContext) + : base(jsonApiContext, resourceRepository) + { + _dbContext = applicationDbContext; + } + + public override ObjectResult Get() + { + return new OkObjectResult(_dbContext.TodoItems.ToList()); + } +} +``` + + ## References [JsonApi Specification](http://jsonapi.org/) From 5f8e176ee90d2419044827cef8424529ef6b6cf6 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 29 Aug 2016 15:39:04 -0500 Subject: [PATCH 02/28] add customizable resource expressions --- JsonApiDotNetCore/Abstractions/JsonApiContext.cs | 2 +- .../Configuration/IJsonApiModelConfiguration.cs | 8 +++++--- .../Configuration/JsonApiConfigurationBuilder.cs | 7 ++++--- .../Configuration/JsonApiModelConfiguration.cs | 10 ++++++---- JsonApiDotNetCoreExample/Startup.cs | 4 ++++ README.md | 14 +++++++++++--- 6 files changed, 31 insertions(+), 14 deletions(-) diff --git a/JsonApiDotNetCore/Abstractions/JsonApiContext.cs b/JsonApiDotNetCore/Abstractions/JsonApiContext.cs index 9a18bf0375..1a1a2fb5e3 100644 --- a/JsonApiDotNetCore/Abstractions/JsonApiContext.cs +++ b/JsonApiDotNetCore/Abstractions/JsonApiContext.cs @@ -24,7 +24,7 @@ public JsonApiContext(HttpContext httpContext, Route route, object dbContext, Js public Type GetJsonApiResourceType() { - return Configuration.ResourceMapDefinitions[Route.BaseModelType]; + return Configuration.ResourceMapDefinitions[Route.BaseModelType].Item1; } public string GetEntityName() diff --git a/JsonApiDotNetCore/Configuration/IJsonApiModelConfiguration.cs b/JsonApiDotNetCore/Configuration/IJsonApiModelConfiguration.cs index 7b4ba67c19..cf5c982c02 100644 --- a/JsonApiDotNetCore/Configuration/IJsonApiModelConfiguration.cs +++ b/JsonApiDotNetCore/Configuration/IJsonApiModelConfiguration.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using AutoMapper; using JsonApiDotNetCore.Abstractions; namespace JsonApiDotNetCore.Configuration @@ -23,10 +24,11 @@ public interface IJsonApiModelConfiguration /// /// Define explicit mapping of a model to a class that implements IJsonApiResource /// - /// - /// + /// + /// + /// /// - void AddResourceMapping(Type modelType, Type resourceType); + void AddResourceMapping(Action mappingExpression); /// /// Specifies a controller override class for a particular model type. diff --git a/JsonApiDotNetCore/Configuration/JsonApiConfigurationBuilder.cs b/JsonApiDotNetCore/Configuration/JsonApiConfigurationBuilder.cs index 0375e75b11..4debb4d227 100644 --- a/JsonApiDotNetCore/Configuration/JsonApiConfigurationBuilder.cs +++ b/JsonApiDotNetCore/Configuration/JsonApiConfigurationBuilder.cs @@ -3,6 +3,7 @@ using JsonApiDotNetCore.Routing; using Microsoft.EntityFrameworkCore; using System.Linq; +using System.Linq.Expressions; using AutoMapper; using JsonApiDotNetCore.Abstractions; using JsonApiDotNetCore.Attributes; @@ -68,10 +69,10 @@ private void SetupResourceMaps() { foreach (var definition in Config.ResourceMapDefinitions) { - cfg.CreateMap(definition.Key, definition.Value); + var mappingExpression = cfg.CreateMap(definition.Key, definition.Value.Item1); + definition.Value.Item2?.Invoke(mappingExpression); } }); - Config.ResourceMapper = mapConfiguration.CreateMapper(); } @@ -87,7 +88,7 @@ private void LoadDefaultResourceMaps() // do not overwrite custom definitions if(!Config.ResourceMapDefinitions.ContainsKey(modelType.UnderlyingSystemType)) { - Config.ResourceMapDefinitions.Add(modelType.UnderlyingSystemType, resourceType); + Config.ResourceMapDefinitions.Add(modelType.UnderlyingSystemType, new Tuple>(resourceType, null)); } } } diff --git a/JsonApiDotNetCore/Configuration/JsonApiModelConfiguration.cs b/JsonApiDotNetCore/Configuration/JsonApiModelConfiguration.cs index 7c4fe2611a..affb0f0767 100644 --- a/JsonApiDotNetCore/Configuration/JsonApiModelConfiguration.cs +++ b/JsonApiDotNetCore/Configuration/JsonApiModelConfiguration.cs @@ -18,7 +18,7 @@ public class JsonApiModelConfiguration : IJsonApiModelConfiguration public IMapper ResourceMapper; public Type ContextType { get; set; } public List Routes = new List(); - public Dictionary ResourceMapDefinitions = new Dictionary(); + public Dictionary>> ResourceMapDefinitions = new Dictionary>>(); public Dictionary ControllerOverrides = new Dictionary(); public void SetDefaultNamespace(string ns) @@ -26,13 +26,15 @@ public void SetDefaultNamespace(string ns) Namespace = ns; } - // TODO: change to AddResourceMapping(Type, Type) - public void AddResourceMapping(Type modelType, Type resourceType) + public void AddResourceMapping(Action mappingExpression) { + var resourceType = typeof(TResource); + var modelType = typeof(TModel); + if (!resourceType.GetInterfaces().Contains(typeof(IJsonApiResource))) throw new ArgumentException("Specified type does not implement IJsonApiResource", nameof(resourceType)); - ResourceMapDefinitions.Add(modelType, resourceType); + ResourceMapDefinitions.Add(modelType, new Tuple>(resourceType, mappingExpression)); } public void UseController(Type modelType, Type controllerType) diff --git a/JsonApiDotNetCoreExample/Startup.cs b/JsonApiDotNetCoreExample/Startup.cs index 638f323e06..e90978750b 100644 --- a/JsonApiDotNetCoreExample/Startup.cs +++ b/JsonApiDotNetCoreExample/Startup.cs @@ -41,6 +41,10 @@ public void ConfigureServices(IServiceCollection services) config.SetDefaultNamespace("api/v1"); config.UseContext(); config.UseController(typeof(TodoItem), typeof(TodoItemsController)); + config.AddResourceMapping(map => + { + map.ForMember("Name", opt => opt.MapFrom(src => $"{((Person)src).Name}_1")); + }); }); } diff --git a/README.md b/README.md index 78f81fbccc..0a5f11e217 100644 --- a/README.md +++ b/README.md @@ -49,11 +49,19 @@ public class PersonResource : IJsonApiResource } ``` -We use [AutoMapper](http://automapper.org/) as the mapping tool. -You can specify a custom mapping configuration in your `Startup` class like so: +We use [AutoMapper](http://automapper.org/) to map from the context model to the JsonApiResource. +The below snippet shows how you can specify a custom mapping expression in your `Startup` class that will apped '_1' to the resource name. +Check out [AutoMapper's Wiki](https://github.com/AutoMapper/AutoMapper/wiki) for detailed mapping options. ``` -// not implemented +services.AddJsonApi(config => { + ... + config.AddResourceMapping(map => + { + map.ForMember("Name", opt => opt.MapFrom(src => $"{((Person)src).Name}_1")); + }); + ... +}); ``` ## Overriding controllers From c9e4b3f5b68f6c4f18aea24c527e0005874b7556 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 29 Aug 2016 15:44:19 -0500 Subject: [PATCH 03/28] add IJsonApiContext --- .../Abstractions/IJsonApiContext.cs | 18 ++++++++++++++++++ .../Abstractions/JsonApiContext.cs | 2 +- .../Controllers/JsonApiController.cs | 4 ++-- 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 JsonApiDotNetCore/Abstractions/IJsonApiContext.cs diff --git a/JsonApiDotNetCore/Abstractions/IJsonApiContext.cs b/JsonApiDotNetCore/Abstractions/IJsonApiContext.cs new file mode 100644 index 0000000000..7c3bdcc410 --- /dev/null +++ b/JsonApiDotNetCore/Abstractions/IJsonApiContext.cs @@ -0,0 +1,18 @@ +using System; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Routing; +using Microsoft.AspNetCore.Http; + +namespace JsonApiDotNetCore.Abstractions +{ + public interface IJsonApiContext + { + JsonApiModelConfiguration Configuration { get; } + object DbContext { get; } + HttpContext HttpContext { get; } + Route Route { get; } + string GetEntityName(); + Type GetEntityType(); + Type GetJsonApiResourceType(); + } +} diff --git a/JsonApiDotNetCore/Abstractions/JsonApiContext.cs b/JsonApiDotNetCore/Abstractions/JsonApiContext.cs index 1a1a2fb5e3..d2eb00f819 100644 --- a/JsonApiDotNetCore/Abstractions/JsonApiContext.cs +++ b/JsonApiDotNetCore/Abstractions/JsonApiContext.cs @@ -7,7 +7,7 @@ namespace JsonApiDotNetCore.Abstractions { - public class JsonApiContext + public class JsonApiContext : IJsonApiContext { public HttpContext HttpContext { get; } public Route Route { get; } diff --git a/JsonApiDotNetCore/Controllers/JsonApiController.cs b/JsonApiDotNetCore/Controllers/JsonApiController.cs index 015963cae8..0b6f14ba7a 100644 --- a/JsonApiDotNetCore/Controllers/JsonApiController.cs +++ b/JsonApiDotNetCore/Controllers/JsonApiController.cs @@ -8,10 +8,10 @@ namespace JsonApiDotNetCore.Controllers { public class JsonApiController : IJsonApiController { - protected readonly JsonApiContext JsonApiContext; + protected readonly IJsonApiContext JsonApiContext; private readonly ResourceRepository _resourceRepository; - public JsonApiController(JsonApiContext jsonApiContext, ResourceRepository resourceRepository) + public JsonApiController(IJsonApiContext jsonApiContext, ResourceRepository resourceRepository) { JsonApiContext = jsonApiContext; _resourceRepository = resourceRepository; From 91f29c1363996bc5f0a788674a149c0601a6f47a Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 29 Aug 2016 15:45:12 -0500 Subject: [PATCH 04/28] update readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0a5f11e217..7dd9084caf 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ services.AddJsonApi(config => { }); ``` -The controller **must** implement `IJsonApiController`, and it **may** inherit from `JsonApiController`. +The controller **must** implement `IJsonApiController`, and it **may** inherit from [JsonApiController](https://github.com/Research-Institute/json-api-dotnet-core/blob/master/JsonApiDotNetCore/Controllers/JsonApiController.cs). Constructor dependency injection will work like normal. Any services added in your `Startup.ConfigureServices()` method will be injected into the constructor parameters. @@ -85,7 +85,7 @@ public class TodoItemsController : JsonApiController, IJsonApiController { private ApplicationDbContext _dbContext; - public TodoItemsController(JsonApiContext jsonApiContext, ResourceRepository resourceRepository, ApplicationDbContext applicationDbContext) + public TodoItemsController(IJsonApiContext jsonApiContext, ResourceRepository resourceRepository, ApplicationDbContext applicationDbContext) : base(jsonApiContext, resourceRepository) { _dbContext = applicationDbContext; @@ -98,6 +98,8 @@ public class TodoItemsController : JsonApiController, IJsonApiController } ``` +You can access the HttpContext from [IJsonApiContext](https://github.com/Research-Institute/json-api-dotnet-core/blob/master/JsonApiDotNetCore/Abstractions/IJsonApiContext.cs). + ## References [JsonApi Specification](http://jsonapi.org/) From 61e75c50117bfca743763d13faaf2e7946d53505 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 29 Aug 2016 15:51:47 -0500 Subject: [PATCH 05/28] update readme --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7dd9084caf..cc1332dcbd 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,10 @@ app.UseJsonApi(); ## Specifying The Presenter / ViewModel -When you define a model, you **must** specify the associated `JsonApiResource` class which **must** implement `IJsonApiResource`. -This is used for mapping out only the data that should be available to client applications. + - When you define a model, you **MUST** specify the associated resource class using the `JsonApiResource` attribute. + - The specified resource class **MUST** implement `IJsonApiResource`. + +The resource class defines how the model will be exposed to client applications. For example: @@ -49,8 +51,8 @@ public class PersonResource : IJsonApiResource } ``` -We use [AutoMapper](http://automapper.org/) to map from the context model to the JsonApiResource. -The below snippet shows how you can specify a custom mapping expression in your `Startup` class that will apped '_1' to the resource name. +We use [AutoMapper](http://automapper.org/) to map from the model class to the resource class. +The below snippet shows how you can specify a custom mapping expression in your `Startup` class that will append `_1` to the resource name. Check out [AutoMapper's Wiki](https://github.com/AutoMapper/AutoMapper/wiki) for detailed mapping options. ``` @@ -58,6 +60,7 @@ services.AddJsonApi(config => { ... config.AddResourceMapping(map => { + // resource.Name = model.Name + "_1" map.ForMember("Name", opt => opt.MapFrom(src => $"{((Person)src).Name}_1")); }); ... @@ -76,7 +79,7 @@ services.AddJsonApi(config => { }); ``` -The controller **must** implement `IJsonApiController`, and it **may** inherit from [JsonApiController](https://github.com/Research-Institute/json-api-dotnet-core/blob/master/JsonApiDotNetCore/Controllers/JsonApiController.cs). +The controller **MUST** implement `IJsonApiController`, and it **MAY** inherit from [JsonApiController](https://github.com/Research-Institute/json-api-dotnet-core/blob/master/JsonApiDotNetCore/Controllers/JsonApiController.cs). Constructor dependency injection will work like normal. Any services added in your `Startup.ConfigureServices()` method will be injected into the constructor parameters. From a983d180a32197ca8f0c7c290fee28ecd1685598 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 29 Aug 2016 15:55:59 -0500 Subject: [PATCH 06/28] refactor UseController method --- .../Configuration/IJsonApiModelConfiguration.cs | 6 +++--- .../Configuration/JsonApiModelConfiguration.cs | 7 +++++-- JsonApiDotNetCoreExample/Startup.cs | 2 +- README.md | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/JsonApiDotNetCore/Configuration/IJsonApiModelConfiguration.cs b/JsonApiDotNetCore/Configuration/IJsonApiModelConfiguration.cs index cf5c982c02..cdb9aefeb5 100644 --- a/JsonApiDotNetCore/Configuration/IJsonApiModelConfiguration.cs +++ b/JsonApiDotNetCore/Configuration/IJsonApiModelConfiguration.cs @@ -33,9 +33,9 @@ public interface IJsonApiModelConfiguration /// /// Specifies a controller override class for a particular model type. /// - /// - /// + /// + /// /// - void UseController(Type modelType, Type controllerType); + void UseController(); } } diff --git a/JsonApiDotNetCore/Configuration/JsonApiModelConfiguration.cs b/JsonApiDotNetCore/Configuration/JsonApiModelConfiguration.cs index affb0f0767..ad7feb42c4 100644 --- a/JsonApiDotNetCore/Configuration/JsonApiModelConfiguration.cs +++ b/JsonApiDotNetCore/Configuration/JsonApiModelConfiguration.cs @@ -37,9 +37,12 @@ public void AddResourceMapping(Action map ResourceMapDefinitions.Add(modelType, new Tuple>(resourceType, mappingExpression)); } - public void UseController(Type modelType, Type controllerType) + public void UseController() { - if(!controllerType.GetInterfaces().Contains(typeof(IJsonApiController))) + var modelType = typeof(TModel); + var controllerType = typeof(TController); + + if (!controllerType.GetInterfaces().Contains(typeof(IJsonApiController))) throw new ArgumentException("Specified type does not implement IJsonApiController", nameof(controllerType)); ControllerOverrides[modelType] = controllerType; diff --git a/JsonApiDotNetCoreExample/Startup.cs b/JsonApiDotNetCoreExample/Startup.cs index e90978750b..5593be0f12 100644 --- a/JsonApiDotNetCoreExample/Startup.cs +++ b/JsonApiDotNetCoreExample/Startup.cs @@ -40,7 +40,7 @@ public void ConfigureServices(IServiceCollection services) services.AddJsonApi(config => { config.SetDefaultNamespace("api/v1"); config.UseContext(); - config.UseController(typeof(TodoItem), typeof(TodoItemsController)); + config.UseController(); config.AddResourceMapping(map => { map.ForMember("Name", opt => opt.MapFrom(src => $"{((Person)src).Name}_1")); diff --git a/README.md b/README.md index cc1332dcbd..c354d076bd 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ You can define your own controllers that implement the `IJsonApiController` like ``` services.AddJsonApi(config => { ... - config.UseController(typeof(TodoItem), typeof(TodoItemsController)); + config.UseController(); ... }); ``` From df887652e5de4c9ab11dd46c3f55a88cf656b38d Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 30 Aug 2016 09:48:56 -0500 Subject: [PATCH 07/28] add extension tests --- .../IServiceCollectionExtensionsTests.cs | 24 +++++++++++--- .../UnitTests/PathStringExtensionsTests.cs | 28 ++++++++++++++++ .../UnitTests/StringExtensionsTests.cs | 32 +++++++++++++++++++ 3 files changed, 80 insertions(+), 4 deletions(-) rename JsonApiDotNetCoreTests/Extensions/{ => UnitTests}/IServiceCollectionExtensionsTests.cs (51%) create mode 100644 JsonApiDotNetCoreTests/Extensions/UnitTests/PathStringExtensionsTests.cs create mode 100644 JsonApiDotNetCoreTests/Extensions/UnitTests/StringExtensionsTests.cs diff --git a/JsonApiDotNetCoreTests/Extensions/IServiceCollectionExtensionsTests.cs b/JsonApiDotNetCoreTests/Extensions/UnitTests/IServiceCollectionExtensionsTests.cs similarity index 51% rename from JsonApiDotNetCoreTests/Extensions/IServiceCollectionExtensionsTests.cs rename to JsonApiDotNetCoreTests/Extensions/UnitTests/IServiceCollectionExtensionsTests.cs index 6ace7845e1..2d25871641 100644 --- a/JsonApiDotNetCoreTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/JsonApiDotNetCoreTests/Extensions/UnitTests/IServiceCollectionExtensionsTests.cs @@ -3,11 +3,10 @@ using JsonApiDotNetCore.Extensions; using JsonApiDotNetCoreTests.Helpers; using Microsoft.EntityFrameworkCore; +using System; namespace JsonApiDotNetCoreTests.Extensions.UnitTests { - // see example explanation on xUnit.net website: - // https://xunit.github.io/docs/getting-started-dotnet-core.html public class IServiceCollectionExtensionsTests { [Fact] @@ -17,12 +16,29 @@ public void AddJsonApi_AddsRouterToServiceCollection() var serviceCollection = new ServiceCollection(); // act - serviceCollection.AddJsonApi(config => { - config.UseContext(); + serviceCollection.AddJsonApi(config => + { + config.UseContext(); }); // assert Assert.True(serviceCollection.ContainsType(typeof(IRouter))); } + + [Fact] + public void AddJsonApi_ThrowsException_IfContextIsNotDefined() + { + // arrange + var serviceCollection = new ServiceCollection(); + + // act + var testAction = new Action(() => + { + serviceCollection.AddJsonApi(config => { }); + }); + + // assert + Assert.Throws(testAction); + } } } diff --git a/JsonApiDotNetCoreTests/Extensions/UnitTests/PathStringExtensionsTests.cs b/JsonApiDotNetCoreTests/Extensions/UnitTests/PathStringExtensionsTests.cs new file mode 100644 index 0000000000..e8926a6f07 --- /dev/null +++ b/JsonApiDotNetCoreTests/Extensions/UnitTests/PathStringExtensionsTests.cs @@ -0,0 +1,28 @@ +using Xunit; +using JsonApiDotNetCore.Extensions; +using Microsoft.AspNetCore.Http; + +namespace JsonApiDotNetCoreTests.Extensions.UnitTests +{ + public class PathStringExtensionsTests + { + [Theory] + [InlineData("/todoItems", "todoItems", "/")] + [InlineData("/todoItems/1", "todoItems", "/1")] + [InlineData("/1/relationships/person", "1", "/relationships/person")] + [InlineData("/relationships/person", "relationships", "/person")] + public void ExtractFirstSegment_Removes_And_Returns_FirstSegementInPathString(string path, string expectedFirstSegment, string expectedRemainder) + { + // arrange + var pathString = new PathString(path); + + // act + PathString remainingPath; + var firstSegment = pathString.ExtractFirstSegment(out remainingPath); + + // assert + Assert.Equal(expectedFirstSegment, firstSegment); + Assert.Equal(expectedRemainder, remainingPath); + } + } +} diff --git a/JsonApiDotNetCoreTests/Extensions/UnitTests/StringExtensionsTests.cs b/JsonApiDotNetCoreTests/Extensions/UnitTests/StringExtensionsTests.cs new file mode 100644 index 0000000000..9d9b3710cd --- /dev/null +++ b/JsonApiDotNetCoreTests/Extensions/UnitTests/StringExtensionsTests.cs @@ -0,0 +1,32 @@ +using Xunit; +using JsonApiDotNetCore.Extensions; + +namespace JsonApiDotNetCoreTests.Extensions.UnitTests +{ + public class StringExtensionsTests + { + [Theory] + [InlineData("TodoItem", "todoItem")] + public void ToCamelCase_ConvertsString_ToCamelCase(string input, string expectedOutput) + { + // arrange + // act + var result = input.ToCamelCase(); + + // assert + Assert.Equal(expectedOutput, result); + } + + [Theory] + [InlineData("todoItem", "TodoItem")] + public void ToProperCase_ConvertsString_ToProperCase(string input, string expectedOutput) + { + // arrange + // act + var result = input.ToProperCase(); + + // assert + Assert.Equal(expectedOutput, result); + } + } +} From e7a446627ffe94bd5760bb6e36fe60543fc3e36c Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 30 Aug 2016 10:36:29 -0500 Subject: [PATCH 08/28] add appveyor stuff --- JsonApiDotNetCore/Build.ps1 | 64 +++++++++++++++++++++ JsonApiDotNetCore/Data/GenericDataAccess.cs | 6 -- JsonApiDotNetCore/LICENSE | 21 +++++++ JsonApiDotNetCore/project.json | 6 +- appveyor.yml | 29 ++++++++++ 5 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 JsonApiDotNetCore/Build.ps1 create mode 100644 JsonApiDotNetCore/LICENSE create mode 100644 appveyor.yml diff --git a/JsonApiDotNetCore/Build.ps1 b/JsonApiDotNetCore/Build.ps1 new file mode 100644 index 0000000000..fb3f5a6317 --- /dev/null +++ b/JsonApiDotNetCore/Build.ps1 @@ -0,0 +1,64 @@ +<# +.SYNOPSIS + You can add this to you build script to ensure that psbuild is available before calling + Invoke-MSBuild. If psbuild is not available locally it will be downloaded automatically. +#> +function EnsurePsbuildInstalled{ + [cmdletbinding()] + param( + [string]$psbuildInstallUri = 'https://raw.githubusercontent.com/ligershark/psbuild/master/src/GetPSBuild.ps1' + ) + process{ + if(-not (Get-Command "Invoke-MsBuild" -errorAction SilentlyContinue)){ + 'Installing psbuild from [{0}]' -f $psbuildInstallUri | Write-Verbose + (new-object Net.WebClient).DownloadString($psbuildInstallUri) | iex + } + else{ + 'psbuild already loaded, skipping download' | Write-Verbose + } + + # make sure it's loaded and throw if not + if(-not (Get-Command "Invoke-MsBuild" -errorAction SilentlyContinue)){ + throw ('Unable to install/load psbuild from [{0}]' -f $psbuildInstallUri) + } + } +} + +# Taken from psake https://github.com/psake/psake + +<# +.SYNOPSIS + This is a helper function that runs a scriptblock and checks the PS variable $lastexitcode + to see if an error occcured. If an error is detected then an exception is thrown. + This function allows you to run command-line programs without having to + explicitly check the $lastexitcode variable. +.EXAMPLE + exec { svn info $repository_trunk } "Error executing SVN. Please verify SVN command-line client is installed" +#> +function Exec +{ + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd, + [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ($msgs.error_bad_command -f $cmd) + ) + & $cmd + if ($lastexitcode -ne 0) { + throw ("Exec: " + $errorMessage) + } +} + +if(Test-Path .\artifacts) { Remove-Item .\artifacts -Force -Recurse } + +EnsurePsbuildInstalled + +exec { & dotnet restore } + +Invoke-MSBuild + +$revision = @{ $true = $env:APPVEYOR_BUILD_NUMBER; $false = 1 }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; +$revision = "{0:D4}" -f [convert]::ToInt32($revision, 10) + +exec { & dotnet test .\JsonApiDotNetCoreTests -c Release } + +exec { & dotnet pack .\JsonApiDotNetCore -c Release -o .\artifacts --version-suffix=$revision } diff --git a/JsonApiDotNetCore/Data/GenericDataAccess.cs b/JsonApiDotNetCore/Data/GenericDataAccess.cs index ef36114349..6205bf05e4 100644 --- a/JsonApiDotNetCore/Data/GenericDataAccess.cs +++ b/JsonApiDotNetCore/Data/GenericDataAccess.cs @@ -1,11 +1,5 @@ -using System; -using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; -using JsonApiDotNetCore.Abstractions; -using System.Reflection; using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Routing; using Microsoft.EntityFrameworkCore; public class GenericDataAccess diff --git a/JsonApiDotNetCore/LICENSE b/JsonApiDotNetCore/LICENSE new file mode 100644 index 0000000000..1b22e225f2 --- /dev/null +++ b/JsonApiDotNetCore/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Children's Research Institute + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/JsonApiDotNetCore/project.json b/JsonApiDotNetCore/project.json index f55c1423f1..e83c090245 100644 --- a/JsonApiDotNetCore/project.json +++ b/JsonApiDotNetCore/project.json @@ -1,5 +1,9 @@ { - "version": "1.0.0-*", + "version": "0.1.0-alpha-*", + "packOptions": { + "licenseUrl": "https://github.com/Research-Institute/json-api-dotnet-core/tree/master/JsonApiDotNetCore/LICENSE", + "projectUrl": "https://github.com/Research-Institute/json-api-dotnet-core/" + }, "dependencies": { "NETStandard.Library": "1.6.0", "Microsoft.AspNetCore.Mvc": "1.0.0", diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000000..32995500a4 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,29 @@ +version: '{build}' +pull_requests: + do_not_increment_build_number: true +branches: + only: + - master +nuget: + disable_publish_on_pr: true +build_script: +- ps: .\Build.ps1 +test: off +artifacts: +- path: .\artifacts\**\*.nupkg + name: NuGet +deploy: +- provider: NuGet + server: https://www.myget.org/F/research-institute/api/v2/package + api_key: + secure: 6CeYcZ4Ze+57gxfeuHzqP6ldbUkPtF6pfpVM1Gw/K2jExFrAz763gNAQ++tiacq3 + skip_symbols: true + on: + branch: master +- provider: NuGet + name: production + api_key: + secure: /fsEOgG4EdtNd6DPmko9h3NxQwx1IGDcFreGTKd2KA56U2KEkpX/L/pCGpCIEf2s + on: + branch: master + appveyor_repo_tag: true From 2de281e19737f819567747e900219c74a889cfe1 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 30 Aug 2016 11:01:30 -0500 Subject: [PATCH 09/28] move build file --- JsonApiDotNetCore/Build.ps1 => Build.ps1 | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename JsonApiDotNetCore/Build.ps1 => Build.ps1 (100%) diff --git a/JsonApiDotNetCore/Build.ps1 b/Build.ps1 similarity index 100% rename from JsonApiDotNetCore/Build.ps1 rename to Build.ps1 From 80a56c4e717086cf58997d172acf71fab8c6ff37 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 30 Aug 2016 11:03:36 -0500 Subject: [PATCH 10/28] add xproj files --- JsonApiDotNetCore/JsonApiDotNetCore.xproj | 19 +++++++++++++++++++ .../JsonApiDotNetCoreExample.xproj | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 JsonApiDotNetCore/JsonApiDotNetCore.xproj create mode 100644 JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.xproj diff --git a/JsonApiDotNetCore/JsonApiDotNetCore.xproj b/JsonApiDotNetCore/JsonApiDotNetCore.xproj new file mode 100644 index 0000000000..cc9985e128 --- /dev/null +++ b/JsonApiDotNetCore/JsonApiDotNetCore.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + f26e89b2-d2fd-406f-baf6-e6c67513c30b + JsonApiDotNetCore + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.xproj b/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.xproj new file mode 100644 index 0000000000..a39d8479f7 --- /dev/null +++ b/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + d1fc385d-412b-4474-8406-a39e9aefd5b0 + JsonApiDotNetCoreExample + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file From 456887c8a0f17bbd7c06abcad4af5d9662e4233a Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 30 Aug 2016 11:43:37 -0500 Subject: [PATCH 11/28] update readme --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c354d076bd..4b0aa14d31 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,21 @@ # JSON API .Net Core +[![Build status](https://ci.appveyor.com/api/projects/status/9fvgeoxdikwkom10?svg=true)](https://ci.appveyor.com/project/jaredcnance/json-api-dotnet-core) + JSON API Spec Conformance: **Non Conforming** -Go [here](https://github.com/Research-Institute/json-api-dotnet-core/wiki/Request-Examples) to see examples of HTTP requests and responses +## Installation + +For pre-releases, add the [MyGet](https://www.myget.org/feed/Details/research-institute) package feed +(https://www.myget.org/F/research-institute/api/v3/index.json) +to your nuget configuration. + +NuGet packages will be published at v0.1.0. ## Usage +Go [here](https://github.com/Research-Institute/json-api-dotnet-core/wiki/Request-Examples) to see examples of HTTP requests and responses + - Configure the service: ``` From a6a0e0ed4262b8f85f26f794530b7d0e7348fac3 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 30 Aug 2016 11:47:14 -0500 Subject: [PATCH 12/28] update package versions --- JsonApiDotNetCoreExample/project.json | 2 +- JsonApiDotNetCoreTests/project.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/JsonApiDotNetCoreExample/project.json b/JsonApiDotNetCoreExample/project.json index 9a96a2251d..b230617faf 100644 --- a/JsonApiDotNetCoreExample/project.json +++ b/JsonApiDotNetCoreExample/project.json @@ -15,7 +15,7 @@ "Microsoft.Extensions.Logging.Console": "1.0.0", "Microsoft.Extensions.Logging.Debug": "1.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0", - "JsonApiDotNetCore": "1.0.0", + "JsonApiDotNetCore": "0.1.0", "Npgsql.EntityFrameworkCore.PostgreSQL": "1.0.0", "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final" }, diff --git a/JsonApiDotNetCoreTests/project.json b/JsonApiDotNetCoreTests/project.json index 1722f539a4..ecc172886b 100644 --- a/JsonApiDotNetCoreTests/project.json +++ b/JsonApiDotNetCoreTests/project.json @@ -2,7 +2,7 @@ "version": "1.0.0-*", "testRunner": "xunit", "dependencies": { - "JsonApiDotNetCore": "1.0.0", + "JsonApiDotNetCore": "0.1.0", "dotnet-test-xunit": "2.2.0-preview2-build1029", "xunit": "2.2.0-beta2-build3300", "moq": "4.6.38-alpha" From 91fb64b6d0df27ddf59e6100c33d29216a5058a0 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 30 Aug 2016 14:00:36 -0500 Subject: [PATCH 13/28] add travis ci --- JsonApiDotNetCore/.travis.yml | 31 +++++++++++++++++++++++++++++++ build.sh | 29 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 JsonApiDotNetCore/.travis.yml create mode 100755 build.sh diff --git a/JsonApiDotNetCore/.travis.yml b/JsonApiDotNetCore/.travis.yml new file mode 100644 index 0000000000..908f6ce5df --- /dev/null +++ b/JsonApiDotNetCore/.travis.yml @@ -0,0 +1,31 @@ +language: csharp +sudo: required +dist: trusty +env: + - CLI_VERSION=latest +addons: + apt: + packages: + - gettext + - libcurl4-openssl-dev + - libicu-dev + - libssl-dev + - libunwind8 + - zlib1g +mono: + - 4.2.3 +os: + - linux + - osx +osx_image: xcode7.1 +branches: + only: + - master +before_install: + - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/; fiopenssl; fi +install: + - export DOTNET_INSTALL_DIR="$PWD/.dotnetcli" + - curl -sSL https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0/scripts/obtain/dotnet-install.sh | bash /dev/stdin --version "$CLI_VERSION" --install-dir "$DOTNET_INSTALL_DIR" + - export PATH="$DOTNET_INSTALL_DIR:$PATH" +script: + - ./build.sh \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000000..218b588051 --- /dev/null +++ b/build.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +#exit if any command fails +set -e + +artifactsFolder="./artifacts" + +if [ -d $artifactsFolder ]; then + rm -R $artifactsFolder +fi + +dotnet restore + +# Ideally we would use the 'dotnet test' command to test netcoreapp and net451 so restrict for now +# but this currently doesn't work due to https://github.com/dotnet/cli/issues/3073 so restrict to netcoreapp + +dotnet test ./JsonApiDotNetCoreTests -c Release -f netcoreapp1.0 + +# Instead, run directly with mono for the full .net version +dotnet build ./JsonApiDotNetCoreTests -c Release -f net451 + +mono \ +./test/JsonApiDotNetCoreTests/bin/Release/net451/*/dotnet-test-xunit.exe \ +./test/JsonApiDotNetCoreTests/bin/Release/net451/*/JsonApiDotNetCoreTests.dll + +revision=${TRAVIS_JOB_ID:=1} +revision=$(printf "%04d" $revision) + +dotnet pack ./JsonApiDotNetCore -c Release -o ./artifacts --version-suffix=$revision From f8e159bac4f341d5a0e07f9ad985cd92d28ac17a Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 30 Aug 2016 14:03:45 -0500 Subject: [PATCH 14/28] move travis yml --- JsonApiDotNetCore/.travis.yml => .travis.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename JsonApiDotNetCore/.travis.yml => .travis.yml (100%) diff --git a/JsonApiDotNetCore/.travis.yml b/.travis.yml similarity index 100% rename from JsonApiDotNetCore/.travis.yml rename to .travis.yml From 2bd1ccf23d4a78bdd6ab5077e4a94394e1f2461c Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 30 Aug 2016 14:04:50 -0500 Subject: [PATCH 15/28] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4b0aa14d31..1d0c5ccf68 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # JSON API .Net Core [![Build status](https://ci.appveyor.com/api/projects/status/9fvgeoxdikwkom10?svg=true)](https://ci.appveyor.com/project/jaredcnance/json-api-dotnet-core) +[![Travis](https://img.shields.io/travis/Research-Institute/json-api-dotnet-core.svg?maxAge=3600&label=travis)](https://travis-ci.org/Research-Institute/json-api-dotnet-core) JSON API Spec Conformance: **Non Conforming** From 37ddbd9985ed97080e527bbb47d6d4edb4ea26ac Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 30 Aug 2016 14:19:46 -0500 Subject: [PATCH 16/28] fix ci build --- JsonApiDotNetCore/project.json | 4 ++-- JsonApiDotNetCoreExample/project.json | 15 ++++++++++----- JsonApiDotNetCoreTests/project.json | 1 + build.sh | 4 ++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/JsonApiDotNetCore/project.json b/JsonApiDotNetCore/project.json index e83c090245..66a3123f23 100644 --- a/JsonApiDotNetCore/project.json +++ b/JsonApiDotNetCore/project.json @@ -19,13 +19,13 @@ "System.Reflection.Extensions": "4.0.1" }, "frameworks": { + "net451": {}, "netstandard1.6": { "imports": "dnxcore50", "dependencies": { "System.Reflection.TypeExtensions": "4.1.0" } - }, - "net452": {} + } }, "tooling": { "defaultNamespace": "JsonApiDotNetCore" diff --git a/JsonApiDotNetCoreExample/project.json b/JsonApiDotNetCoreExample/project.json index b230617faf..5d78b38044 100644 --- a/JsonApiDotNetCoreExample/project.json +++ b/JsonApiDotNetCoreExample/project.json @@ -1,9 +1,5 @@ { "dependencies": { - "Microsoft.NETCore.App": { - "version": "1.0.0", - "type": "platform" - }, "Microsoft.AspNetCore.Mvc": "1.0.0", "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", @@ -30,7 +26,14 @@ } }, "frameworks": { + "net451": {}, "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.0.0", + "type": "platform" + } + }, "imports": [ "dotnet5.6", "portable-net45+win8" @@ -42,7 +45,9 @@ "preserveCompilationContext": true, "debugType": "portable", "copyToOutput": { - "include": ["appsettings.json"] + "include": [ + "appsettings.json" + ] } }, "runtimeOptions": { diff --git a/JsonApiDotNetCoreTests/project.json b/JsonApiDotNetCoreTests/project.json index ecc172886b..5daef55eaf 100644 --- a/JsonApiDotNetCoreTests/project.json +++ b/JsonApiDotNetCoreTests/project.json @@ -8,6 +8,7 @@ "moq": "4.6.38-alpha" }, "frameworks": { + "net451": {}, "netcoreapp1.0": { "imports": [ "dotnet5.6", diff --git a/build.sh b/build.sh index 218b588051..7fe560f8e1 100755 --- a/build.sh +++ b/build.sh @@ -20,8 +20,8 @@ dotnet test ./JsonApiDotNetCoreTests -c Release -f netcoreapp1.0 dotnet build ./JsonApiDotNetCoreTests -c Release -f net451 mono \ -./test/JsonApiDotNetCoreTests/bin/Release/net451/*/dotnet-test-xunit.exe \ -./test/JsonApiDotNetCoreTests/bin/Release/net451/*/JsonApiDotNetCoreTests.dll +./JsonApiDotNetCoreTests/bin/Release/net451/*/dotnet-test-xunit.exe \ +./JsonApiDotNetCoreTests/bin/Release/net451/*/JsonApiDotNetCoreTests.dll revision=${TRAVIS_JOB_ID:=1} revision=$(printf "%04d" $revision) From abdfa5c8e5c22d6b6a792264ccb52887deaf435f Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 30 Aug 2016 15:40:26 -0500 Subject: [PATCH 17/28] add singleOrDefault expression tree builder --- JsonApiDotNetCore/Data/GenericDataAccess.cs | 40 ++++++++++++++++---- JsonApiDotNetCore/Data/ResourceRepository.cs | 27 ++++++------- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/JsonApiDotNetCore/Data/GenericDataAccess.cs b/JsonApiDotNetCore/Data/GenericDataAccess.cs index 6205bf05e4..6b42f5a4f5 100644 --- a/JsonApiDotNetCore/Data/GenericDataAccess.cs +++ b/JsonApiDotNetCore/Data/GenericDataAccess.cs @@ -1,16 +1,42 @@ +using System; +using System.Reflection; using System.Linq; +using System.Linq.Expressions; using JsonApiDotNetCore.Extensions; using Microsoft.EntityFrameworkCore; -public class GenericDataAccess +namespace JsonApiDotNetCore.Data { - public DbSet GetDbSet(DbContext context) where T : class + public class GenericDataAccess { - return context.Set(); - } + public DbSet GetDbSet(DbContext context) where T : class + { + return context.Set(); + } - public IQueryable IncludeEntity(IQueryable queryable, string includedEntityName) where T : class - { - return queryable.Include(includedEntityName); + public IQueryable IncludeEntity(IQueryable queryable, string includedEntityName) where T : class + { + return queryable.Include(includedEntityName); + } + + public T SingleOrDefault(object query, string param, string value) + { + var queryable = (IQueryable) query; + var currentType = queryable.ElementType; + var property = currentType.GetProperty(param); + + if (property == null) + { + throw new ArgumentException($"'{param}' is not a valid property of '{currentType}'"); + } + + var prm = Expression.Parameter(currentType, property.Name); + var left = Expression.Convert(Expression.PropertyOrField(prm, property.Name), typeof(string)); + var right = Expression.Constant(1, property.PropertyType); + var body = Expression.Equal(left, right); + var where = Expression.Lambda>(body, prm); + + return queryable.SingleOrDefault(where); + } } } diff --git a/JsonApiDotNetCore/Data/ResourceRepository.cs b/JsonApiDotNetCore/Data/ResourceRepository.cs index 7b850ed1c3..a0f01b26d1 100644 --- a/JsonApiDotNetCore/Data/ResourceRepository.cs +++ b/JsonApiDotNetCore/Data/ResourceRepository.cs @@ -25,18 +25,14 @@ public List Get() public object Get(string id) { - if (_context.Route is RelationalRoute) - { - return GetRelated(id, _context.Route as RelationalRoute); - } - return GetEntityById(_context.Route.BaseModelType, id, null); + var route = _context.Route as RelationalRoute; + return route != null ? GetRelated(id, route) : GetEntityById(_context.Route.BaseModelType, id, null); } private object GetRelated(string id, RelationalRoute relationalRoute) { - // HACK: this would rely on lazy loading to work...will probably fail var entity = GetEntityById(relationalRoute.BaseModelType, id, relationalRoute.RelationshipName); - return relationalRoute.BaseModelType.GetProperties().FirstOrDefault(pi => pi.Name.ToCamelCase() == relationalRoute.RelationshipName.ToCamelCase()).GetValue(entity); + return relationalRoute.BaseModelType.GetProperties().FirstOrDefault(pi => pi.Name.ToCamelCase() == relationalRoute.RelationshipName.ToCamelCase())?.GetValue(entity); } private IQueryable GetDbSetFromContext(string propName) @@ -47,13 +43,13 @@ private IQueryable GetDbSetFromContext(string propName) private object GetEntityById(Type modelType, string id, string includedRelationship) { - // HACK: I _believe_ by casting to IEnumerable, we are loading all records into memory, if so... find a better way... - // Also, we are making a BIG assumption that the resource has an attribute Id and not ResourceId which is allowed by EF + // get generic dbSet var dataAccessorInstance = Activator.CreateInstance(typeof(GenericDataAccess)); - var dataAccessorMethod = dataAccessorInstance.GetType().GetMethod("GetDbSet"); - var genericMethod = dataAccessorMethod.MakeGenericMethod(modelType); - var dbSet = genericMethod.Invoke(dataAccessorInstance, new [] {((DbContext) _context.DbContext) }); + var dataAccessorGetDbSetMethod = dataAccessorInstance.GetType().GetMethod("GetDbSet"); + var genericGetDbSetMethod = dataAccessorGetDbSetMethod.MakeGenericMethod(modelType); + var dbSet = genericGetDbSetMethod.Invoke(dataAccessorInstance, new [] {((DbContext) _context.DbContext) }); + // include relationships if requested if (!string.IsNullOrEmpty(includedRelationship)) { var includeMethod = dataAccessorInstance.GetType().GetMethod("IncludeEntity"); @@ -61,7 +57,12 @@ private object GetEntityById(Type modelType, string id, string includedRelations dbSet = genericIncludeMethod.Invoke(dataAccessorInstance, new []{ dbSet, includedRelationship.ToProperCase() }); } - return (dbSet as IEnumerable).SingleOrDefault(x => x.Id.ToString() == id); + // get the SingleOrDefault value by Id + var dataAccessorSingleOrDefaultMethod = dataAccessorInstance.GetType().GetMethod("SingleOrDefault"); + var genericSingleOrDefaultMethod = dataAccessorSingleOrDefaultMethod.MakeGenericMethod(modelType); + var entity = genericSingleOrDefaultMethod.Invoke(dataAccessorInstance, new[] { dbSet, "Id", id }); + + return entity; } public void Add(object entity) From 7c0f6a8aeb080df75b651f0f71fbfbf1a325e877 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 30 Aug 2016 16:11:20 -0500 Subject: [PATCH 18/28] add vscode launch.json and tasks.json --- JsonApiDotNetCoreExample/.vscode/launch.json | 52 ++++++++++++++++++++ JsonApiDotNetCoreExample/.vscode/tasks.json | 17 +++++++ 2 files changed, 69 insertions(+) create mode 100644 JsonApiDotNetCoreExample/.vscode/launch.json create mode 100644 JsonApiDotNetCoreExample/.vscode/tasks.json diff --git a/JsonApiDotNetCoreExample/.vscode/launch.json b/JsonApiDotNetCoreExample/.vscode/launch.json new file mode 100644 index 0000000000..739e3e85e9 --- /dev/null +++ b/JsonApiDotNetCoreExample/.vscode/launch.json @@ -0,0 +1,52 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceRoot}/bin/Debug/netcoreapp1.0/JsonApiDotNetCoreExample.dll", + "args": [], + "cwd": "${workspaceRoot}", + "stopAtEntry": false, + "externalConsole": false + }, + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceRoot}/bin/Debug/netcoreapp1.0/JsonApiDotNetCoreExample.dll", + "args": [], + "cwd": "${workspaceRoot}", + "stopAtEntry": false, + "launchBrowser": { + "enabled": true, + "args": "${auto-detect-url}", + "windows": { + "command": "cmd.exe", + "args": "/C start ${auto-detect-url}" + }, + "osx": { + "command": "open" + }, + "linux": { + "command": "xdg-open" + } + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceRoot}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command.pickProcess}" + } + ] +} diff --git a/JsonApiDotNetCoreExample/.vscode/tasks.json b/JsonApiDotNetCoreExample/.vscode/tasks.json new file mode 100644 index 0000000000..33256db7ac --- /dev/null +++ b/JsonApiDotNetCoreExample/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "0.1.0", + "command": "dotnet", + "isShellCommand": true, + "args": [], + "tasks": [ + { + "taskName": "build", + "args": [ ], + "isBuildCommand": true, + "showOutput": "silent", + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file From 7418787f485c4d8e1e721dc9201ef8b58b870566 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 30 Aug 2016 16:35:45 -0500 Subject: [PATCH 19/28] add GenericDataAccessAbstraction Closes #9 --- JsonApiDotNetCore/Data/GenericDataAccess.cs | 15 ++++-- .../Data/GenericDataAccessAbstraction.cs | 51 +++++++++++++++++++ JsonApiDotNetCore/Data/ResourceRepository.cs | 21 +------- 3 files changed, 63 insertions(+), 24 deletions(-) create mode 100644 JsonApiDotNetCore/Data/GenericDataAccessAbstraction.cs diff --git a/JsonApiDotNetCore/Data/GenericDataAccess.cs b/JsonApiDotNetCore/Data/GenericDataAccess.cs index 6b42f5a4f5..64a804b366 100644 --- a/JsonApiDotNetCore/Data/GenericDataAccess.cs +++ b/JsonApiDotNetCore/Data/GenericDataAccess.cs @@ -19,7 +19,7 @@ public IQueryable IncludeEntity(IQueryable queryable, string includedEn return queryable.Include(includedEntityName); } - public T SingleOrDefault(object query, string param, string value) + public T SingleOrDefault(object query, string param, object value) { var queryable = (IQueryable) query; var currentType = queryable.ElementType; @@ -30,9 +30,16 @@ public T SingleOrDefault(object query, string param, string value) throw new ArgumentException($"'{param}' is not a valid property of '{currentType}'"); } - var prm = Expression.Parameter(currentType, property.Name); - var left = Expression.Convert(Expression.PropertyOrField(prm, property.Name), typeof(string)); - var right = Expression.Constant(1, property.PropertyType); + // convert the incoming value to the target value type + // "1" -> 1 + var convertedValue = Convert.ChangeType(value, property.PropertyType); + // {model} + var prm = Expression.Parameter(currentType, "model"); + // {model.Id} + var left = Expression.PropertyOrField(prm, property.Name); + // {1} + var right = Expression.Constant(convertedValue, property.PropertyType); + // {model.Id == 1} var body = Expression.Equal(left, right); var where = Expression.Lambda>(body, prm); diff --git a/JsonApiDotNetCore/Data/GenericDataAccessAbstraction.cs b/JsonApiDotNetCore/Data/GenericDataAccessAbstraction.cs new file mode 100644 index 0000000000..51b59d1dac --- /dev/null +++ b/JsonApiDotNetCore/Data/GenericDataAccessAbstraction.cs @@ -0,0 +1,51 @@ +using System; +using System.Reflection; +using System.Linq; +using System.Linq.Expressions; +using JsonApiDotNetCore.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace JsonApiDotNetCore.Data +{ + public class GenericDataAccessAbstraction + { + private GenericDataAccess _dataAccessorInstance; + private DbContext _dbContext; + private Type _modelType; + private string _includedRelationship; + public GenericDataAccessAbstraction(object dbContext, Type modelType, string includedRelationship) + { + _dataAccessorInstance = (GenericDataAccess)Activator.CreateInstance(typeof(GenericDataAccess)); + _dbContext = (DbContext) dbContext; + _modelType = modelType; + _includedRelationship = includedRelationship?.ToProperCase(); + } + + public object SingleOrDefault(string id) + { + var dbSet = GetDbSet(); + if (!string.IsNullOrEmpty(_includedRelationship)) + { + dbSet = IncludeRelationshipInContext(dbSet); + } + var dataAccessorSingleOrDefaultMethod = _dataAccessorInstance.GetType().GetMethod("SingleOrDefault"); + var genericSingleOrDefaultMethod = dataAccessorSingleOrDefaultMethod.MakeGenericMethod(_modelType); + return genericSingleOrDefaultMethod.Invoke(_dataAccessorInstance, new[] { dbSet, "Id", id }); + } + + private object GetDbSet() + { + var dataAccessorGetDbSetMethod = _dataAccessorInstance.GetType().GetMethod("GetDbSet"); + var genericGetDbSetMethod = dataAccessorGetDbSetMethod.MakeGenericMethod(_modelType); + return genericGetDbSetMethod.Invoke(_dataAccessorInstance, new [] { _dbContext }); + } + + private object IncludeRelationshipInContext(object dbSet) + { + var includeMethod = _dataAccessorInstance.GetType().GetMethod("IncludeEntity"); + var genericIncludeMethod = includeMethod.MakeGenericMethod(_modelType); + return genericIncludeMethod.Invoke(_dataAccessorInstance, new []{ dbSet, _includedRelationship }); + } + + } +} diff --git a/JsonApiDotNetCore/Data/ResourceRepository.cs b/JsonApiDotNetCore/Data/ResourceRepository.cs index a0f01b26d1..ee2d2597bf 100644 --- a/JsonApiDotNetCore/Data/ResourceRepository.cs +++ b/JsonApiDotNetCore/Data/ResourceRepository.cs @@ -43,26 +43,7 @@ private IQueryable GetDbSetFromContext(string propName) private object GetEntityById(Type modelType, string id, string includedRelationship) { - // get generic dbSet - var dataAccessorInstance = Activator.CreateInstance(typeof(GenericDataAccess)); - var dataAccessorGetDbSetMethod = dataAccessorInstance.GetType().GetMethod("GetDbSet"); - var genericGetDbSetMethod = dataAccessorGetDbSetMethod.MakeGenericMethod(modelType); - var dbSet = genericGetDbSetMethod.Invoke(dataAccessorInstance, new [] {((DbContext) _context.DbContext) }); - - // include relationships if requested - if (!string.IsNullOrEmpty(includedRelationship)) - { - var includeMethod = dataAccessorInstance.GetType().GetMethod("IncludeEntity"); - var genericIncludeMethod = includeMethod.MakeGenericMethod(modelType); - dbSet = genericIncludeMethod.Invoke(dataAccessorInstance, new []{ dbSet, includedRelationship.ToProperCase() }); - } - - // get the SingleOrDefault value by Id - var dataAccessorSingleOrDefaultMethod = dataAccessorInstance.GetType().GetMethod("SingleOrDefault"); - var genericSingleOrDefaultMethod = dataAccessorSingleOrDefaultMethod.MakeGenericMethod(modelType); - var entity = genericSingleOrDefaultMethod.Invoke(dataAccessorInstance, new[] { dbSet, "Id", id }); - - return entity; + return new GenericDataAccessAbstraction(_context.DbContext, modelType, includedRelationship).SingleOrDefault(id);; } public void Add(object entity) From 690ea21a84b5854611c71769bf57ed4305d9387f Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 30 Aug 2016 21:04:11 -0500 Subject: [PATCH 20/28] remove net451 dependency test proj --- JsonApiDotNetCoreTests/project.json | 5 ++--- build.sh | 10 ---------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/JsonApiDotNetCoreTests/project.json b/JsonApiDotNetCoreTests/project.json index 5daef55eaf..92e9ee614d 100644 --- a/JsonApiDotNetCoreTests/project.json +++ b/JsonApiDotNetCoreTests/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-*", + "version": "1.0.0-alpha", "testRunner": "xunit", "dependencies": { "JsonApiDotNetCore": "0.1.0", @@ -8,7 +8,6 @@ "moq": "4.6.38-alpha" }, "frameworks": { - "net451": {}, "netcoreapp1.0": { "imports": [ "dotnet5.6", @@ -33,7 +32,7 @@ "defaultNamespace": "JsonApiDotNetCoreTests" }, "tools": { - "Microsoft.DotNet.Watcher.Tools": { + "Microsoft.DotNet.Watcher.Tools": { "version": "1.0.0-*", "imports": "portable-net451+win8" } diff --git a/build.sh b/build.sh index 7fe560f8e1..e9eddc94a9 100755 --- a/build.sh +++ b/build.sh @@ -11,18 +11,8 @@ fi dotnet restore -# Ideally we would use the 'dotnet test' command to test netcoreapp and net451 so restrict for now -# but this currently doesn't work due to https://github.com/dotnet/cli/issues/3073 so restrict to netcoreapp - dotnet test ./JsonApiDotNetCoreTests -c Release -f netcoreapp1.0 -# Instead, run directly with mono for the full .net version -dotnet build ./JsonApiDotNetCoreTests -c Release -f net451 - -mono \ -./JsonApiDotNetCoreTests/bin/Release/net451/*/dotnet-test-xunit.exe \ -./JsonApiDotNetCoreTests/bin/Release/net451/*/JsonApiDotNetCoreTests.dll - revision=${TRAVIS_JOB_ID:=1} revision=$(printf "%04d" $revision) From 8830064f9d40defa63caa7dffcf83ae7ebc3e03c Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 30 Aug 2016 21:05:25 -0500 Subject: [PATCH 21/28] add SingleOrDefault test --- .../Data/TestData/TestContext.cs | 14 ++++++ .../Data/TestData/TodoItem.cs | 8 ++++ .../Data/UnitTests/GenericDataAccessTests.cs | 43 +++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 JsonApiDotNetCoreTests/Data/TestData/TestContext.cs create mode 100644 JsonApiDotNetCoreTests/Data/TestData/TodoItem.cs create mode 100644 JsonApiDotNetCoreTests/Data/UnitTests/GenericDataAccessTests.cs diff --git a/JsonApiDotNetCoreTests/Data/TestData/TestContext.cs b/JsonApiDotNetCoreTests/Data/TestData/TestContext.cs new file mode 100644 index 0000000000..a36a00c428 --- /dev/null +++ b/JsonApiDotNetCoreTests/Data/TestData/TestContext.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; + +namespace JsonApiDotNetCoreTests.Data.TestData +{ + public class TestContext : DbContext + { + public TestContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet TodoItems { get; set; } + } +} diff --git a/JsonApiDotNetCoreTests/Data/TestData/TodoItem.cs b/JsonApiDotNetCoreTests/Data/TestData/TodoItem.cs new file mode 100644 index 0000000000..9acbf979d8 --- /dev/null +++ b/JsonApiDotNetCoreTests/Data/TestData/TodoItem.cs @@ -0,0 +1,8 @@ +namespace JsonApiDotNetCoreTests.Data.TestData +{ + public class TodoItem + { + public int Id { get; set; } + public string Name { get; set; } + } +} diff --git a/JsonApiDotNetCoreTests/Data/UnitTests/GenericDataAccessTests.cs b/JsonApiDotNetCoreTests/Data/UnitTests/GenericDataAccessTests.cs new file mode 100644 index 0000000000..e675bd7f76 --- /dev/null +++ b/JsonApiDotNetCoreTests/Data/UnitTests/GenericDataAccessTests.cs @@ -0,0 +1,43 @@ +using JsonApiDotNetCoreTests.Data.TestData; +using Microsoft.EntityFrameworkCore; +using Moq; +using Xunit; +using JsonApiDotNetCore.Data; +using System.Linq; +using System; +using System.Collections.Generic; + +namespace JsonApiDotNetCoreTests.Data.UnitTests +{ + public class GenericDataAccessTests + { + [Fact] + public void SingleOrDefault_Fetches_SingleItemFromContext() + { + // arrange + var data = new List + { + new TodoItem { Id = 1, Name = "AAA" }, + new TodoItem { Id = 2, Name = "BBB" } + }.AsQueryable(); + + //var mockSet = new Mock>(); + var mockSet = new Mock>(); + mockSet.As>().Setup(m => m.Provider).Returns(data.Provider); + mockSet.As>().Setup(m => m.Expression).Returns(data.Expression); + mockSet.As>().Setup(m => m.ElementType).Returns(data.ElementType); + mockSet.As>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator()); + + + var genericDataAccess = new GenericDataAccess(); + + // act + var item1 = genericDataAccess.SingleOrDefault(mockSet.Object, "Id", 1); + var item2 = genericDataAccess.SingleOrDefault(mockSet.Object, "Id", 2); + + // assert + Assert.Equal(1, item1.Id); + Assert.Equal(2, item2.Id); + } + } +} From 5f276bbb8145598bceda19b54e193e7c39a1197f Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 31 Aug 2016 08:45:31 -0500 Subject: [PATCH 22/28] attempt to fix travis.yml --- .travis.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 908f6ce5df..51ff0aa69f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ -language: csharp -sudo: required -dist: trusty -env: +language: csharp +sudo: required +dist: trusty +env: - CLI_VERSION=latest -addons: +addons: apt: packages: - gettext @@ -12,20 +12,20 @@ addons: - libssl-dev - libunwind8 - zlib1g -mono: +mono: - 4.2.3 -os: +os: - linux - osx -osx_image: xcode7.1 -branches: +osx_image: xcode7.1 +branches: only: - master -before_install: - - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/; fiopenssl; fi -install: +before_install: + - if test "$TRAVIS_OS_NAME" = "osx"; then brew update; brew install openssl; ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/; fi openssl; fi +install: - export DOTNET_INSTALL_DIR="$PWD/.dotnetcli" - curl -sSL https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0/scripts/obtain/dotnet-install.sh | bash /dev/stdin --version "$CLI_VERSION" --install-dir "$DOTNET_INSTALL_DIR" - - export PATH="$DOTNET_INSTALL_DIR:$PATH" -script: - - ./build.sh \ No newline at end of file + - export PATH="$DOTNET_INSTALL_DIR:$PATH" +script: + - ./build.sh From 15232148701a4706bf1cdce9ceb64adcbb7e01bd Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 31 Aug 2016 08:45:54 -0500 Subject: [PATCH 23/28] update SingleOrDefault to test attr other than Id --- .../Data/UnitTests/GenericDataAccessTests.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/JsonApiDotNetCoreTests/Data/UnitTests/GenericDataAccessTests.cs b/JsonApiDotNetCoreTests/Data/UnitTests/GenericDataAccessTests.cs index e675bd7f76..43fd4aa2a7 100644 --- a/JsonApiDotNetCoreTests/Data/UnitTests/GenericDataAccessTests.cs +++ b/JsonApiDotNetCoreTests/Data/UnitTests/GenericDataAccessTests.cs @@ -4,7 +4,6 @@ using Xunit; using JsonApiDotNetCore.Data; using System.Linq; -using System; using System.Collections.Generic; namespace JsonApiDotNetCoreTests.Data.UnitTests @@ -21,19 +20,17 @@ public void SingleOrDefault_Fetches_SingleItemFromContext() new TodoItem { Id = 2, Name = "BBB" } }.AsQueryable(); - //var mockSet = new Mock>(); var mockSet = new Mock>(); mockSet.As>().Setup(m => m.Provider).Returns(data.Provider); mockSet.As>().Setup(m => m.Expression).Returns(data.Expression); mockSet.As>().Setup(m => m.ElementType).Returns(data.ElementType); mockSet.As>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator()); - var genericDataAccess = new GenericDataAccess(); // act var item1 = genericDataAccess.SingleOrDefault(mockSet.Object, "Id", 1); - var item2 = genericDataAccess.SingleOrDefault(mockSet.Object, "Id", 2); + var item2 = genericDataAccess.SingleOrDefault(mockSet.Object, "Name", "BBB"); // assert Assert.Equal(1, item1.Id); From 451843685e7950491c5d7924a44e1400a43bacf0 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 31 Aug 2016 09:18:25 -0500 Subject: [PATCH 24/28] fix travis yml syntax --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 51ff0aa69f..4c276999f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ branches: only: - master before_install: - - if test "$TRAVIS_OS_NAME" = "osx"; then brew update; brew install openssl; ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/; fi openssl; fi + - if test "$TRAVIS_OS_NAME" = "osx"; then brew update; brew install openssl; ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/; fi install: - export DOTNET_INSTALL_DIR="$PWD/.dotnetcli" - curl -sSL https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0/scripts/obtain/dotnet-install.sh | bash /dev/stdin --version "$CLI_VERSION" --install-dir "$DOTNET_INSTALL_DIR" From 526aa5f31c12def274e7cf46427fabae520af64f Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 31 Aug 2016 09:39:06 -0500 Subject: [PATCH 25/28] add where data accessor --- JsonApiDotNetCore/Data/GenericDataAccess.cs | 27 +++++++++++++++++ .../Data/GenericDataAccessAbstraction.cs | 30 +++++++++++++------ JsonApiDotNetCore/Data/ResourceRepository.cs | 2 +- .../Data/UnitTests/GenericDataAccessTests.cs | 26 ++++++++++++++++ 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/JsonApiDotNetCore/Data/GenericDataAccess.cs b/JsonApiDotNetCore/Data/GenericDataAccess.cs index 64a804b366..2844f4748f 100644 --- a/JsonApiDotNetCore/Data/GenericDataAccess.cs +++ b/JsonApiDotNetCore/Data/GenericDataAccess.cs @@ -45,5 +45,32 @@ public T SingleOrDefault(object query, string param, object value) return queryable.SingleOrDefault(where); } + + public IQueryable Where(object query, string param, object value) + { + var queryable = (IQueryable) query; + var currentType = queryable.ElementType; + var property = currentType.GetProperty(param); + + if (property == null) + { + throw new ArgumentException($"'{param}' is not a valid property of '{currentType}'"); + } + + // convert the incoming value to the target value type + // "1" -> 1 + var convertedValue = Convert.ChangeType(value, property.PropertyType); + // {model} + var prm = Expression.Parameter(currentType, "model"); + // {model.Id} + var left = Expression.PropertyOrField(prm, property.Name); + // {1} + var right = Expression.Constant(convertedValue, property.PropertyType); + // {model.Id == 1} + var body = Expression.Equal(left, right); + var where = Expression.Lambda>(body, prm); + + return queryable.Where(where); + } } } diff --git a/JsonApiDotNetCore/Data/GenericDataAccessAbstraction.cs b/JsonApiDotNetCore/Data/GenericDataAccessAbstraction.cs index 51b59d1dac..e4ecb707b8 100644 --- a/JsonApiDotNetCore/Data/GenericDataAccessAbstraction.cs +++ b/JsonApiDotNetCore/Data/GenericDataAccessAbstraction.cs @@ -21,23 +21,35 @@ public GenericDataAccessAbstraction(object dbContext, Type modelType, string inc _includedRelationship = includedRelationship?.ToProperCase(); } - public object SingleOrDefault(string id) + public object SingleOrDefault(string propertyName, string value) { var dbSet = GetDbSet(); - if (!string.IsNullOrEmpty(_includedRelationship)) - { - dbSet = IncludeRelationshipInContext(dbSet); - } - var dataAccessorSingleOrDefaultMethod = _dataAccessorInstance.GetType().GetMethod("SingleOrDefault"); - var genericSingleOrDefaultMethod = dataAccessorSingleOrDefaultMethod.MakeGenericMethod(_modelType); - return genericSingleOrDefaultMethod.Invoke(_dataAccessorInstance, new[] { dbSet, "Id", id }); + return InvokeGenericDataAccessMethod("SingleOrDefault", new[] { dbSet, propertyName, value }); + } + + public object Filter(string propertyName, string value) + { + var dbSet = GetDbSet(); + return InvokeGenericDataAccessMethod("Where", new[] { dbSet, propertyName, value }); + } + + private object InvokeGenericDataAccessMethod(string methodName, params object[] propertyValues) + { + var dataAccessorMethod = _dataAccessorInstance.GetType().GetMethod(methodName); + var genericDataAccessorMethod = dataAccessorMethod.MakeGenericMethod(_modelType); + return genericDataAccessorMethod.Invoke(_dataAccessorInstance, propertyValues); } private object GetDbSet() { var dataAccessorGetDbSetMethod = _dataAccessorInstance.GetType().GetMethod("GetDbSet"); var genericGetDbSetMethod = dataAccessorGetDbSetMethod.MakeGenericMethod(_modelType); - return genericGetDbSetMethod.Invoke(_dataAccessorInstance, new [] { _dbContext }); + var dbSet = genericGetDbSetMethod.Invoke(_dataAccessorInstance, new [] { _dbContext }); + if (!string.IsNullOrEmpty(_includedRelationship)) + { + dbSet = IncludeRelationshipInContext(dbSet); + } + return dbSet; } private object IncludeRelationshipInContext(object dbSet) diff --git a/JsonApiDotNetCore/Data/ResourceRepository.cs b/JsonApiDotNetCore/Data/ResourceRepository.cs index ee2d2597bf..77dfb5ed9d 100644 --- a/JsonApiDotNetCore/Data/ResourceRepository.cs +++ b/JsonApiDotNetCore/Data/ResourceRepository.cs @@ -43,7 +43,7 @@ private IQueryable GetDbSetFromContext(string propName) private object GetEntityById(Type modelType, string id, string includedRelationship) { - return new GenericDataAccessAbstraction(_context.DbContext, modelType, includedRelationship).SingleOrDefault(id);; + return new GenericDataAccessAbstraction(_context.DbContext, modelType, includedRelationship).SingleOrDefault("Id", id); } public void Add(object entity) diff --git a/JsonApiDotNetCoreTests/Data/UnitTests/GenericDataAccessTests.cs b/JsonApiDotNetCoreTests/Data/UnitTests/GenericDataAccessTests.cs index 43fd4aa2a7..d7419e719d 100644 --- a/JsonApiDotNetCoreTests/Data/UnitTests/GenericDataAccessTests.cs +++ b/JsonApiDotNetCoreTests/Data/UnitTests/GenericDataAccessTests.cs @@ -36,5 +36,31 @@ public void SingleOrDefault_Fetches_SingleItemFromContext() Assert.Equal(1, item1.Id); Assert.Equal(2, item2.Id); } + + [Fact] + public void Where_FetchesRecords_WherePropertyValueEquals_ProvidedValue() + { + // arrange + var data = new List + { + new TodoItem { Id = 1, Name = "AAA" }, + new TodoItem { Id = 2, Name = "AAA" }, + new TodoItem { Id = 3, Name = "BBB" } + }.AsQueryable(); + + var mockSet = new Mock>(); + mockSet.As>().Setup(m => m.Provider).Returns(data.Provider); + mockSet.As>().Setup(m => m.Expression).Returns(data.Expression); + mockSet.As>().Setup(m => m.ElementType).Returns(data.ElementType); + mockSet.As>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator()); + + var genericDataAccess = new GenericDataAccess(); + + // act + var items = genericDataAccess.Where(mockSet.Object, "Name", "AAA"); + + // assert + Assert.Equal(2, items.Count()); + } } } From 48ae63f5825f24260aa241ddfbd319480ce8846a Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 31 Aug 2016 10:33:20 -0500 Subject: [PATCH 26/28] refactor(GenericDataAccess): consolidate query code --- JsonApiDotNetCore/Data/GenericDataAccess.cs | 36 ++++++--------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/JsonApiDotNetCore/Data/GenericDataAccess.cs b/JsonApiDotNetCore/Data/GenericDataAccess.cs index 2844f4748f..d1f8d6c8db 100644 --- a/JsonApiDotNetCore/Data/GenericDataAccess.cs +++ b/JsonApiDotNetCore/Data/GenericDataAccess.cs @@ -22,34 +22,20 @@ public IQueryable IncludeEntity(IQueryable queryable, string includedEn public T SingleOrDefault(object query, string param, object value) { var queryable = (IQueryable) query; - var currentType = queryable.ElementType; - var property = currentType.GetProperty(param); - - if (property == null) - { - throw new ArgumentException($"'{param}' is not a valid property of '{currentType}'"); - } - - // convert the incoming value to the target value type - // "1" -> 1 - var convertedValue = Convert.ChangeType(value, property.PropertyType); - // {model} - var prm = Expression.Parameter(currentType, "model"); - // {model.Id} - var left = Expression.PropertyOrField(prm, property.Name); - // {1} - var right = Expression.Constant(convertedValue, property.PropertyType); - // {model.Id == 1} - var body = Expression.Equal(left, right); - var where = Expression.Lambda>(body, prm); - - return queryable.SingleOrDefault(where); + var expression = GetEqualityExpressionForProperty(queryable, param, value); + return queryable.SingleOrDefault(expression); } public IQueryable Where(object query, string param, object value) { var queryable = (IQueryable) query; - var currentType = queryable.ElementType; + var expression = GetEqualityExpressionForProperty(queryable, param, value); + return queryable.Where(expression); + } + + private Expression> GetEqualityExpressionForProperty(IQueryable query, string param, object value) + { + var currentType = query.ElementType; var property = currentType.GetProperty(param); if (property == null) @@ -68,9 +54,7 @@ public IQueryable Where(object query, string param, object value) var right = Expression.Constant(convertedValue, property.PropertyType); // {model.Id == 1} var body = Expression.Equal(left, right); - var where = Expression.Lambda>(body, prm); - - return queryable.Where(where); + return Expression.Lambda>(body, prm); } } } From 46736538d35c675c0e176c8fdfb39feb735ae7c9 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 31 Aug 2016 12:08:27 -0500 Subject: [PATCH 27/28] Add filtering --- .../Data/GenericDataAccessAbstraction.cs | 6 +- JsonApiDotNetCore/Data/ResourceRepository.cs | 24 ++++++-- .../Routing/Query/FilterQuery.cs | 13 +++++ JsonApiDotNetCore/Routing/Query/QuerySet.cs | 58 +++++++++++++++++++ .../Routing/Query/SortDirection.cs | 8 +++ .../Routing/Query/SortParameter.cs | 13 +++++ JsonApiDotNetCore/Routing/RelationalRoute.cs | 5 +- JsonApiDotNetCore/Routing/Route.cs | 5 +- JsonApiDotNetCore/Routing/RouteBuilder.cs | 9 ++- .../Routing/UnitTests/Query/QuerySetTests.cs | 34 +++++++++++ .../Routing/UnitTests/RouterTests.cs | 4 +- README.md | 3 +- 12 files changed, 165 insertions(+), 17 deletions(-) create mode 100644 JsonApiDotNetCore/Routing/Query/FilterQuery.cs create mode 100644 JsonApiDotNetCore/Routing/Query/QuerySet.cs create mode 100644 JsonApiDotNetCore/Routing/Query/SortDirection.cs create mode 100644 JsonApiDotNetCore/Routing/Query/SortParameter.cs create mode 100644 JsonApiDotNetCoreTests/Routing/UnitTests/Query/QuerySetTests.cs diff --git a/JsonApiDotNetCore/Data/GenericDataAccessAbstraction.cs b/JsonApiDotNetCore/Data/GenericDataAccessAbstraction.cs index e4ecb707b8..eecc29cd5a 100644 --- a/JsonApiDotNetCore/Data/GenericDataAccessAbstraction.cs +++ b/JsonApiDotNetCore/Data/GenericDataAccessAbstraction.cs @@ -27,10 +27,10 @@ public object SingleOrDefault(string propertyName, string value) return InvokeGenericDataAccessMethod("SingleOrDefault", new[] { dbSet, propertyName, value }); } - public object Filter(string propertyName, string value) + public IQueryable Filter(string propertyName, string value) { var dbSet = GetDbSet(); - return InvokeGenericDataAccessMethod("Where", new[] { dbSet, propertyName, value }); + return (IQueryable)InvokeGenericDataAccessMethod("Where", new[] { dbSet, propertyName, value }); } private object InvokeGenericDataAccessMethod(string methodName, params object[] propertyValues) @@ -40,7 +40,7 @@ private object InvokeGenericDataAccessMethod(string methodName, params object[] return genericDataAccessorMethod.Invoke(_dataAccessorInstance, propertyValues); } - private object GetDbSet() + public object GetDbSet() { var dataAccessorGetDbSetMethod = _dataAccessorInstance.GetType().GetMethod("GetDbSet"); var genericGetDbSetMethod = dataAccessorGetDbSetMethod.MakeGenericMethod(_modelType); diff --git a/JsonApiDotNetCore/Data/ResourceRepository.cs b/JsonApiDotNetCore/Data/ResourceRepository.cs index 77dfb5ed9d..6b5ed59611 100644 --- a/JsonApiDotNetCore/Data/ResourceRepository.cs +++ b/JsonApiDotNetCore/Data/ResourceRepository.cs @@ -6,6 +6,7 @@ using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Routing; using Microsoft.EntityFrameworkCore; +using System.Collections; namespace JsonApiDotNetCore.Data { @@ -20,7 +21,15 @@ public ResourceRepository(JsonApiContext context) public List Get() { - return (GetDbSetFromContext(_context.Route.BaseRouteDefinition.ContextPropertyName) as IEnumerable)?.ToList(); + IQueryable dbSet; + var filter = _context.Route.Query.Filter; + if(filter != null) { + dbSet = FilterEntities(_context.Route.BaseModelType, filter.PropertyName, filter.PropertyValue, null); + } + else { + dbSet = GetDbSet(_context.Route.BaseModelType, null); + } + return ((IEnumerable)dbSet).ToList(); } public object Get(string id) @@ -35,10 +44,10 @@ private object GetRelated(string id, RelationalRoute relationalRoute) return relationalRoute.BaseModelType.GetProperties().FirstOrDefault(pi => pi.Name.ToCamelCase() == relationalRoute.RelationshipName.ToCamelCase())?.GetValue(entity); } - private IQueryable GetDbSetFromContext(string propName) + private IQueryable GetDbSet(Type modelType, string includedRelationship) { var dbContext = _context.DbContext; - return (IQueryable)dbContext.GetType().GetProperties().FirstOrDefault(pI => pI.Name.ToProperCase() == propName.ToProperCase())?.GetValue(dbContext, null); + return (IQueryable)new GenericDataAccessAbstraction(_context.DbContext, modelType, includedRelationship).GetDbSet(); } private object GetEntityById(Type modelType, string id, string includedRelationship) @@ -46,9 +55,14 @@ private object GetEntityById(Type modelType, string id, string includedRelations return new GenericDataAccessAbstraction(_context.DbContext, modelType, includedRelationship).SingleOrDefault("Id", id); } + private IQueryable FilterEntities(Type modelType, string property, string value, string includedRelationship) + { + return new GenericDataAccessAbstraction(_context.DbContext, modelType, includedRelationship).Filter(property, value); + } + public void Add(object entity) { - var dbSet = GetDbSetFromContext(_context.Route.BaseRouteDefinition.ContextPropertyName); + var dbSet = GetDbSet(_context.Route.BaseModelType, null); var dbSetAddMethod = dbSet.GetType().GetMethod("Add"); dbSetAddMethod.Invoke(dbSet, new [] { entity }); } @@ -56,7 +70,7 @@ public void Add(object entity) public void Delete(string id) { var entity = Get(id); - var dbSet = GetDbSetFromContext(_context.Route.BaseRouteDefinition.ContextPropertyName); + var dbSet = GetDbSet(_context.Route.BaseModelType, null); var dbSetAddMethod = dbSet.GetType().GetMethod("Remove"); dbSetAddMethod.Invoke(dbSet, new [] { entity }); } diff --git a/JsonApiDotNetCore/Routing/Query/FilterQuery.cs b/JsonApiDotNetCore/Routing/Query/FilterQuery.cs new file mode 100644 index 0000000000..e4525bd0df --- /dev/null +++ b/JsonApiDotNetCore/Routing/Query/FilterQuery.cs @@ -0,0 +1,13 @@ +namespace JsonApiDotNetCore.Routing.Query +{ + public class FilterQuery + { + public FilterQuery(string propertyName, string propertyValue) + { + PropertyName = propertyName; + PropertyValue = propertyValue; + } + public string PropertyName { get; set; } + public string PropertyValue { get; set; } + } +} diff --git a/JsonApiDotNetCore/Routing/Query/QuerySet.cs b/JsonApiDotNetCore/Routing/Query/QuerySet.cs new file mode 100644 index 0000000000..c3c62f8d96 --- /dev/null +++ b/JsonApiDotNetCore/Routing/Query/QuerySet.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Extensions; +using Microsoft.AspNetCore.Http; + +namespace JsonApiDotNetCore.Routing.Query +{ + public class QuerySet + { + public QuerySet (IQueryCollection query) + { + BuildQuerySet(query); + } + public FilterQuery Filter { get; set; } + public List SortParameters { get; set; } + + private void BuildQuerySet(IQueryCollection query) + { + foreach (var pair in query) + { + if(pair.Key.StartsWith("filter")) + { + Filter = ParseFilterQuery(pair.Key, pair.Value); + continue; + } + + if(pair.Key.StartsWith("sort")){ + SortParameters = ParseSortParameters(pair.Value); + } + } + } + + private FilterQuery ParseFilterQuery(string key, string value) + { + // expected input = filter[id]=1 + var propertyName = key.Split('[', ']')[1].ToProperCase(); + return new FilterQuery(propertyName, value); + } + + // sort=id,name + // sort=-id + private List ParseSortParameters(string value) + { + var sortParameters = new List(); + value.Split(',').ToList().ForEach(p => { + var direction = SortDirection.Ascending; + if(p[0] == '-') + { + direction = SortDirection.Descending; + p = p.Substring(1); + } + sortParameters.Add(new SortParameter(direction, p.ToProperCase())); + }); + + return sortParameters; + } + } +} diff --git a/JsonApiDotNetCore/Routing/Query/SortDirection.cs b/JsonApiDotNetCore/Routing/Query/SortDirection.cs new file mode 100644 index 0000000000..8f724b89a4 --- /dev/null +++ b/JsonApiDotNetCore/Routing/Query/SortDirection.cs @@ -0,0 +1,8 @@ +namespace JsonApiDotNetCore.Routing.Query +{ + public enum SortDirection + { + Ascending = 1, + Descending = 2 + } +} diff --git a/JsonApiDotNetCore/Routing/Query/SortParameter.cs b/JsonApiDotNetCore/Routing/Query/SortParameter.cs new file mode 100644 index 0000000000..ec2af7a5ad --- /dev/null +++ b/JsonApiDotNetCore/Routing/Query/SortParameter.cs @@ -0,0 +1,13 @@ +namespace JsonApiDotNetCore.Routing.Query +{ + public class SortParameter + { + public SortParameter(SortDirection direction, string propertyName) + { + Direction = direction; + PropertyName = propertyName; + } + public SortDirection Direction { get; set; } + public string PropertyName { get; set; } + } +} diff --git a/JsonApiDotNetCore/Routing/RelationalRoute.cs b/JsonApiDotNetCore/Routing/RelationalRoute.cs index 9b7d1f3a68..24aacfc98a 100644 --- a/JsonApiDotNetCore/Routing/RelationalRoute.cs +++ b/JsonApiDotNetCore/Routing/RelationalRoute.cs @@ -1,11 +1,12 @@ using System; +using JsonApiDotNetCore.Routing.Query; namespace JsonApiDotNetCore.Routing { public class RelationalRoute : Route { - public RelationalRoute(Type baseModelType, string requestMethod, string resourceId, RouteDefinition baseRouteDefinition, Type relationalType, string relationshipName) - : base(baseModelType, requestMethod, resourceId, baseRouteDefinition) + public RelationalRoute(Type baseModelType, string requestMethod, string resourceId, RouteDefinition baseRouteDefinition, QuerySet querySet, Type relationalType, string relationshipName) + : base(baseModelType, requestMethod, resourceId, baseRouteDefinition, querySet) { RelationalType = relationalType; RelationshipName = relationshipName; diff --git a/JsonApiDotNetCore/Routing/Route.cs b/JsonApiDotNetCore/Routing/Route.cs index 33cbcfbb08..e6ec551de3 100644 --- a/JsonApiDotNetCore/Routing/Route.cs +++ b/JsonApiDotNetCore/Routing/Route.cs @@ -1,20 +1,23 @@ using System; +using JsonApiDotNetCore.Routing.Query; namespace JsonApiDotNetCore.Routing { public class Route { - public Route(Type baseModelType, string requestMethod, string resourceId, RouteDefinition baseRouteDefinition) + public Route(Type baseModelType, string requestMethod, string resourceId, RouteDefinition baseRouteDefinition, QuerySet query) { BaseModelType = baseModelType; RequestMethod = requestMethod; ResourceId = resourceId; BaseRouteDefinition = baseRouteDefinition; + Query = query; } public Type BaseModelType { get; set; } public string RequestMethod { get; set; } public RouteDefinition BaseRouteDefinition { get; set; } public string ResourceId { get; set; } + public QuerySet Query { get; set; } } } diff --git a/JsonApiDotNetCore/Routing/RouteBuilder.cs b/JsonApiDotNetCore/Routing/RouteBuilder.cs index a36d1a95ef..49fe16092a 100644 --- a/JsonApiDotNetCore/Routing/RouteBuilder.cs +++ b/JsonApiDotNetCore/Routing/RouteBuilder.cs @@ -2,6 +2,7 @@ using JsonApiDotNetCore.Abstractions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Routing.Query; using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.Routing @@ -21,15 +22,17 @@ public Route BuildFromRequest(HttpRequest request) { var remainingPathString = SetBaseRouteDefinition(request.Path); + var querySet = new QuerySet(request.Query); + if (PathStringIsEmpty(remainingPathString)) { // {baseResource} - return new Route(_baseRouteDefinition.ModelType, request.Method, null, _baseRouteDefinition); + return new Route(_baseRouteDefinition.ModelType, request.Method, null, _baseRouteDefinition, querySet); } remainingPathString = SetBaseResourceId(remainingPathString); if (PathStringIsEmpty(remainingPathString)) { // {baseResource}/{baseResourceId} - return new Route(_baseRouteDefinition.ModelType, request.Method, _baseResourceId, _baseRouteDefinition); + return new Route(_baseRouteDefinition.ModelType, request.Method, _baseResourceId, _baseRouteDefinition, querySet); } // { baseResource}/{ baseResourceId}/{relatedResourceName} @@ -41,7 +44,7 @@ public Route BuildFromRequest(HttpRequest request) } var relationshipType = GetTypeOfRelatedResource(relatedResource); - return new RelationalRoute(_baseRouteDefinition.ModelType, request.Method, _baseResourceId, _baseRouteDefinition, relationshipType, relatedResource); + return new RelationalRoute(_baseRouteDefinition.ModelType, request.Method, _baseResourceId, _baseRouteDefinition, querySet, relationshipType, relatedResource); } private bool PathStringIsEmpty(PathString pathString) diff --git a/JsonApiDotNetCoreTests/Routing/UnitTests/Query/QuerySetTests.cs b/JsonApiDotNetCoreTests/Routing/UnitTests/Query/QuerySetTests.cs new file mode 100644 index 0000000000..ce4e4a797a --- /dev/null +++ b/JsonApiDotNetCoreTests/Routing/UnitTests/Query/QuerySetTests.cs @@ -0,0 +1,34 @@ +using Xunit; +using JsonApiDotNetCore.Routing.Query; +using Microsoft.AspNetCore.Http.Internal; +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; +using System.Linq; + +namespace JsonApiDotNetCoreTests.Routing.UnitTests.Query +{ + public class QuerySetTests + { + [Fact] + public void QuerySetConstructor_BuildsObject_FromQueryCollection() + { + // arrange + var queries = new Dictionary(); + queries.Add("filter[id]", "1"); + queries.Add("sort", new StringValues(new string[] { "-id", "name" })); + var queryCollection = new QueryCollection(queries); + + // act + var querySet = new QuerySet(queryCollection); + + // assert + Assert.NotNull(querySet.Filter); + Assert.Equal("Id", querySet.Filter.PropertyName); + Assert.Equal("1", querySet.Filter.PropertyValue); + + Assert.NotNull(querySet.SortParameters); + Assert.NotNull(querySet.SortParameters.SingleOrDefault(x=> x.Direction == SortDirection.Descending && x.PropertyName == "Id")); + Assert.NotNull(querySet.SortParameters.SingleOrDefault(x=> x.Direction == SortDirection.Ascending && x.PropertyName == "Name")); + } + } +} diff --git a/JsonApiDotNetCoreTests/Routing/UnitTests/RouterTests.cs b/JsonApiDotNetCoreTests/Routing/UnitTests/RouterTests.cs index a09b25d0cb..064027fbf7 100644 --- a/JsonApiDotNetCoreTests/Routing/UnitTests/RouterTests.cs +++ b/JsonApiDotNetCoreTests/Routing/UnitTests/RouterTests.cs @@ -46,7 +46,7 @@ public void HandleJsonApiRoute_CallsGetMethod_ForGetRequest() httpResponseMock.Setup(r => r.Body).Returns(new MemoryStream()); httpContextMock.Setup(c => c.Response).Returns(httpResponseMock.Object); - var route = new Route(null, "GET", null, null); + var route = new Route(null, "GET", null, null, null); var routeBuilderMock = new Mock(); routeBuilderMock.Setup(rb => rb.BuildFromRequest(null)).Returns(route); @@ -79,7 +79,7 @@ public void HandleJsonApiRoute_CallsGetIdMethod_ForGetIdRequest() httpResponseMock.Setup(r => r.Body).Returns(new MemoryStream()); httpContextMock.Setup(c => c.Response).Returns(httpResponseMock.Object); - var route = new Route(null, "GET", resourceId, null); + var route = new Route(null, "GET", resourceId, null, null); var routeBuilderMock = new Mock(); routeBuilderMock.Setup(rb => rb.BuildFromRequest(null)).Returns(route); diff --git a/README.md b/README.md index 1d0c5ccf68..25f44364d3 100644 --- a/README.md +++ b/README.md @@ -118,9 +118,10 @@ You can access the HttpContext from [IJsonApiContext](https://github.com/Researc ## References [JsonApi Specification](http://jsonapi.org/) -## Current Assumptions +## Current Entity Requirements - Using Entity Framework - All entities in the specified context should have controllers - All entities are served from the same namespace (i.e. 'api/v1') - All entities have a primary key "Id" and not "EntityId" +- All entity names are proper case, "Id" not "id" From a464431f1e63c4f7dc7629a920db73f24a61401f Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 31 Aug 2016 12:32:50 -0500 Subject: [PATCH 28/28] respond 404 if the route is not defined --- .../Middleware/JsonApiMiddleware.cs | 10 +++++++++- JsonApiDotNetCore/Routing/RouteBuilder.cs | 17 ++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index ddf90a057f..6cfd048888 100644 --- a/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -29,7 +29,9 @@ public async Task Invoke(HttpContext context) _logger.LogInformation("Passing request to JsonApiService: " + context.Request.Path); if(context.Request.ContentType == "application/vnd.api+json") { - _router.HandleJsonApiRoute(context, _serviceProvider); + var routeWasHandled = _router.HandleJsonApiRoute(context, _serviceProvider); + if(!routeWasHandled) + RespondNotFound(context); } else { @@ -46,5 +48,11 @@ private void RespondUnsupportedMediaType(HttpContext context) context.Response.StatusCode = 415; context.Response.Body.Flush(); } + + private void RespondNotFound(HttpContext context) + { + context.Response.StatusCode = 404; + context.Response.Body.Flush(); + } } } diff --git a/JsonApiDotNetCore/Routing/RouteBuilder.cs b/JsonApiDotNetCore/Routing/RouteBuilder.cs index 49fe16092a..167d5b9abc 100644 --- a/JsonApiDotNetCore/Routing/RouteBuilder.cs +++ b/JsonApiDotNetCore/Routing/RouteBuilder.cs @@ -20,7 +20,10 @@ public RouteBuilder(JsonApiModelConfiguration configuration) public Route BuildFromRequest(HttpRequest request) { - var remainingPathString = SetBaseRouteDefinition(request.Path); + PathString remainingPathString; + _baseRouteDefinition = SetBaseRouteDefinition(request.Path, out remainingPathString); + + if(_baseRouteDefinition == null) return null; var querySet = new QuerySet(request.Query); @@ -52,18 +55,18 @@ private bool PathStringIsEmpty(PathString pathString) return pathString.HasValue ? string.IsNullOrEmpty(pathString.ToString().TrimStart('/')) : true; } - private PathString SetBaseRouteDefinition(PathString path) + private RouteDefinition SetBaseRouteDefinition(PathString path, out PathString remainingPath) { + PathString remainingPathTemp; foreach (var rte in _configuration.Routes) { - PathString remainingPathString; - if (path.StartsWithSegments(new PathString(rte.PathString), StringComparison.OrdinalIgnoreCase, out remainingPathString)) + if (path.StartsWithSegments(new PathString(rte.PathString), StringComparison.OrdinalIgnoreCase, out remainingPathTemp)) { - _baseRouteDefinition = rte; - return remainingPathString; + remainingPath = remainingPathTemp; + return rte; } } - throw new Exception("Route is not defined."); + return null; } private PathString SetBaseResourceId(PathString remainPathString)