From 99f623d130e778399c98d544e63c7044302bd308 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 17:18:56 -0500 Subject: [PATCH 01/14] feat(IQueryable): add extension for selecting columns by list of names --- .../Extensions/IQueryableExtensions.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 2e2551603c..6cecb52762 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -1,4 +1,3 @@ - using System; using System.Linq; using System.Linq.Expressions; @@ -126,5 +125,25 @@ public static IQueryable Filter(this IQueryable sourc throw new JsonApiException("400", $"Could not cast {filterQuery.PropertyValue} to {property.PropertyType.Name}"); } } + public static IQueryable Select(this IQueryable source, string[] columns) + { + var sourceType = source.ElementType; + + var resultType = typeof(TSource); + + // {model} + var parameter = Expression.Parameter(sourceType, "model"); + + var bindings = columns.Select(column => Expression.Bind( + resultType.GetProperty(column), Expression.PropertyOrField(parameter, column))); + + var body = Expression.MemberInit(Expression.New(resultType), bindings); + + var selector = Expression.Lambda(body, parameter); + + return source.Provider.CreateQuery( + Expression.Call(typeof(Queryable), "Select", new Type[] { sourceType, resultType }, + source.Expression, Expression.Quote(selector))); + } } } From 1b8b6cbb0fbdc211bbbac4b18dd13f111d22a344 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 20:52:05 -0500 Subject: [PATCH 02/14] fix(IQueryableExt): clean up extension --- .../Extensions/IQueryableExtensions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 6cecb52762..2c12bc78aa 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -125,7 +125,7 @@ public static IQueryable Filter(this IQueryable sourc throw new JsonApiException("400", $"Could not cast {filterQuery.PropertyValue} to {property.PropertyType.Name}"); } } - public static IQueryable Select(this IQueryable source, string[] columns) + public static IQueryable Select(this IQueryable source, string[] columns) { var sourceType = source.ElementType; @@ -137,13 +137,13 @@ public static IQueryable Select(this IQueryable source, string var bindings = columns.Select(column => Expression.Bind( resultType.GetProperty(column), Expression.PropertyOrField(parameter, column))); + // { new Model () { Property = model.Property } } var body = Expression.MemberInit(Expression.New(resultType), bindings); - var selector = Expression.Lambda(body, parameter); + // { model => new TodoItem() { Property = model.Property } } + var selector = Expression.Lambda>(body, parameter); - return source.Provider.CreateQuery( - Expression.Call(typeof(Queryable), "Select", new Type[] { sourceType, resultType }, - source.Expression, Expression.Quote(selector))); + return source.Select(selector); } } } From aa6faf40efb458808f2acb5117ddf6f642e974b6 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 20:53:14 -0500 Subject: [PATCH 03/14] test(sparse-fieldsets): validate the use of the Select extension --- .../Acceptance/Spec/SparseFieldSetTests.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs new file mode 100644 index 0000000000..14e3c874a1 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs @@ -0,0 +1,50 @@ +using System.Threading.Tasks; +using DotNetCoreDocs; +using DotNetCoreDocs.Writers; +using JsonApiDotNetCoreExample; +using Xunit; +using Microsoft.EntityFrameworkCore; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCoreExample.Models; +using System.Linq; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec +{ + [Collection("WebHostCollection")] + public class SparseFieldSetTests + { + private DocsFixture _fixture; + private readonly AppDbContext _dbContext; + + public SparseFieldSetTests(DocsFixture fixture) + { + _fixture = fixture; + _dbContext = fixture.GetService(); + } + + [Fact] + public async Task Can_Select_Sparse_Fieldsets() + { + // arrange + var fields = new string[] { "Id", "Description" }; + var todoItem = new TodoItem { + Description = "description", + Ordinal = 1 + }; + _dbContext.TodoItems.Add(todoItem); + await _dbContext.SaveChangesAsync(); + + // act + var result = await _dbContext + .TodoItems + .Where(t=>t.Id == todoItem.Id) + .Select(fields) + .FirstAsync(); + + // assert + Assert.Equal(0, result.Ordinal); + Assert.Equal(todoItem.Description, result.Description); + } + } +} From d50fb51d8847f6c01630c0a5e26e4a7574db21ba Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 21:17:19 -0500 Subject: [PATCH 04/14] clean(error-test): remove unused ns --- .../Acceptance/Extensibility/CustomErrorTests.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs index 3ba7d18156..ce2b541f5b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs @@ -1,11 +1,7 @@ -using DotNetCoreDocs; -using JsonApiDotNetCoreExample; -using DotNetCoreDocs.Writers; using Newtonsoft.Json; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Serialization; using Xunit; -using System.Diagnostics; namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility { @@ -14,8 +10,6 @@ public class CustomErrorTests [Fact] public void Can_Return_Custom_Error_Types() { - // while(!Debugger.IsAttached) { bool stop = false; } - // arrange var error = new CustomError("507", "title", "detail", "custom"); var errorCollection = new ErrorCollection(); From a2ab5bd27c0559ea51624340dc61a449c6a3abd8 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 21:49:54 -0500 Subject: [PATCH 05/14] feat(query-set): parse fields parameter --- .../Internal/Query/QuerySet.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs index 864251e4a4..58f1c189f1 100644 --- a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs +++ b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs @@ -19,6 +19,7 @@ public QuerySet( _jsonApiContext = jsonApiContext; PageQuery = new PageQuery(); Filters = new List(); + Fields = new List(); BuildQuerySet(query); } @@ -26,6 +27,7 @@ public QuerySet( public PageQuery PageQuery { get; set; } public List SortParameters { get; set; } public List IncludedRelationships { get; set; } + public List Fields { get; set; } private void BuildQuerySet(IQueryCollection query) { @@ -55,6 +57,12 @@ private void BuildQuerySet(IQueryCollection query) continue; } + if (pair.Key.StartsWith("fields")) + { + Fields = ParseFieldsQuery(pair.Key, pair.Value); + continue; + } + throw new JsonApiException("400", $"{pair} is not a valid query."); } } @@ -160,6 +168,29 @@ private List ParseIncludedRelationships(string value) .ToList(); } + private List ParseFieldsQuery(string key, string value) + { + // expected: fields[TYPE]=prop1,prop2 + var typeName = key.Split('[', ']')[1]; + + var includedFields = new List { "Id" }; + + if(typeName != _jsonApiContext.RequestEntity.EntityName.Dasherize()) + return includedFields; + + var fields = value.Split(','); + foreach(var field in fields) + { + var internalAttrName = _jsonApiContext.RequestEntity + .Attributes + .SingleOrDefault(attr => attr.PublicAttributeName == field) + .InternalAttributeName; + includedFields.Add(internalAttrName); + } + + return includedFields; + } + private AttrAttribute GetAttribute(string propertyName) { return _jsonApiContext.RequestEntity.Attributes From 027b3b0daa02e79a30975aa203844edc8096a9de Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 21:50:17 -0500 Subject: [PATCH 06/14] fix(IQueryableExt): do not return dynamic type --- .../Extensions/IQueryableExtensions.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 2c12bc78aa..abd1686a22 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -125,8 +126,11 @@ public static IQueryable Filter(this IQueryable sourc throw new JsonApiException("400", $"Could not cast {filterQuery.PropertyValue} to {property.PropertyType.Name}"); } } - public static IQueryable Select(this IQueryable source, string[] columns) + public static IQueryable Select(this IQueryable source, IEnumerable columns) { + if(columns == null || columns.Count() == 0) + return source; + var sourceType = source.ElementType; var resultType = typeof(TSource); @@ -141,9 +145,11 @@ public static IQueryable Select(this IQueryable sourc var body = Expression.MemberInit(Expression.New(resultType), bindings); // { model => new TodoItem() { Property = model.Property } } - var selector = Expression.Lambda>(body, parameter); - - return source.Select(selector); + var selector = Expression.Lambda(body, parameter); + + return source.Provider.CreateQuery( + Expression.Call(typeof(Queryable), "Select", new Type[] { sourceType, resultType }, + source.Expression, Expression.Quote(selector))); } } } From 66c0bd22971e6f2c31affb0e2f6689841d72ee5e Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 21:50:37 -0500 Subject: [PATCH 07/14] feat(repository): apply select query --- src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 6e18aebc96..62102bec7b 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -48,7 +48,7 @@ public DefaultEntityRepository( public virtual IQueryable Get() { - return _dbSet; + return _dbSet.Select(_jsonApiContext.QuerySet?.Fields); } public virtual IQueryable Filter(IQueryable entities, FilterQuery filterQuery) @@ -76,12 +76,12 @@ public virtual IQueryable Sort(IQueryable entities, List GetAsync(TId id) { - return await _dbSet.SingleOrDefaultAsync(e => e.Id.Equals(id)); + return await Get().SingleOrDefaultAsync(e => e.Id.Equals(id)); } public virtual async Task GetAndIncludeAsync(TId id, string relationshipName) { - return await _dbSet + return await Get() .Include(relationshipName) .SingleOrDefaultAsync(e => e.Id.Equals(id)); } From b3c3f16fe0c06f1e1058406c9a1cd4927e152a44 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 21:51:07 -0500 Subject: [PATCH 08/14] feat(document-builder): check whether or not an attribute should be inc --- .../Builders/DocumentBuilder.cs | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 12710a4927..92c785c5d0 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -33,12 +33,12 @@ public Document Build(IIdentifiable entity) var document = new Document { - Data = _getData(contextEntity, entity), - Meta = _getMeta(entity), + Data = GetData(contextEntity, entity), + Meta = GetMeta(entity), Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext)) }; - document.Included = _appendIncludedObject(document.Included, contextEntity, entity); + document.Included = AppendIncludedObject(document.Included, contextEntity, entity); return document; } @@ -54,20 +54,20 @@ public Documents Build(IEnumerable entities) var documents = new Documents { Data = new List(), - Meta = _getMeta(entities.FirstOrDefault()), + Meta = GetMeta(entities.FirstOrDefault()), Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext)) }; foreach (var entity in entities) { - documents.Data.Add(_getData(contextEntity, entity)); - documents.Included = _appendIncludedObject(documents.Included, contextEntity, entity); + documents.Data.Add(GetData(contextEntity, entity)); + documents.Included = AppendIncludedObject(documents.Included, contextEntity, entity); } return documents; } - private Dictionary _getMeta(IIdentifiable entity) + private Dictionary GetMeta(IIdentifiable entity) { if (entity == null) return null; @@ -87,9 +87,9 @@ private Dictionary _getMeta(IIdentifiable entity) return null; } - private List _appendIncludedObject(List includedObject, ContextEntity contextEntity, IIdentifiable entity) + private List AppendIncludedObject(List includedObject, ContextEntity contextEntity, IIdentifiable entity) { - var includedEntities = _getIncludedEntities(contextEntity, entity); + var includedEntities = GetIncludedEntities(contextEntity, entity); if (includedEntities.Count > 0) { if (includedObject == null) @@ -100,7 +100,7 @@ private List _appendIncludedObject(List includedObje return includedObject; } - private DocumentData _getData(ContextEntity contextEntity, IIdentifiable entity) + private DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity) { var data = new DocumentData { @@ -115,16 +115,24 @@ private DocumentData _getData(ContextEntity contextEntity, IIdentifiable entity) contextEntity.Attributes.ForEach(attr => { - data.Attributes.Add(attr.PublicAttributeName, attr.GetValue(entity)); + if(ShouldIncludeAttribute(attr)) + data.Attributes.Add(attr.PublicAttributeName, attr.GetValue(entity)); }); if (contextEntity.Relationships.Count > 0) - _addRelationships(data, contextEntity, entity); + AddRelationships(data, contextEntity, entity); return data; } - private void _addRelationships(DocumentData data, ContextEntity contextEntity, IIdentifiable entity) + private bool ShouldIncludeAttribute(AttrAttribute attr) + { + return (_jsonApiContext.QuerySet == null + || _jsonApiContext.QuerySet.Fields.Count == 0 + || _jsonApiContext.QuerySet.Fields.Contains(attr.InternalAttributeName)); + } + + private void AddRelationships(DocumentData data, ContextEntity contextEntity, IIdentifiable entity) { data.Relationships = new Dictionary(); var linkBuilder = new LinkBuilder(_jsonApiContext); @@ -140,7 +148,7 @@ private void _addRelationships(DocumentData data, ContextEntity contextEntity, I } }; - if (_relationshipIsIncluded(r.InternalRelationshipName)) + if (RelationshipIsIncluded(r.InternalRelationshipName)) { var navigationEntity = _jsonApiContext.ContextGraph .GetRelationship(entity, r.InternalRelationshipName); @@ -148,49 +156,49 @@ private void _addRelationships(DocumentData data, ContextEntity contextEntity, I if(navigationEntity == null) relationshipData.SingleData = null; else if (navigationEntity is IEnumerable) - relationshipData.ManyData = _getRelationships((IEnumerable)navigationEntity, r.InternalRelationshipName); + relationshipData.ManyData = GetRelationships((IEnumerable)navigationEntity, r.InternalRelationshipName); else - relationshipData.SingleData = _getRelationship(navigationEntity, r.InternalRelationshipName); + relationshipData.SingleData = GetRelationship(navigationEntity, r.InternalRelationshipName); } data.Relationships.Add(r.InternalRelationshipName.Dasherize(), relationshipData); }); } - private List _getIncludedEntities(ContextEntity contextEntity, IIdentifiable entity) + private List GetIncludedEntities(ContextEntity contextEntity, IIdentifiable entity) { var included = new List(); contextEntity.Relationships.ForEach(r => { - if (!_relationshipIsIncluded(r.InternalRelationshipName)) return; + if (!RelationshipIsIncluded(r.InternalRelationshipName)) return; var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, r.InternalRelationshipName); if (navigationEntity is IEnumerable) foreach (var includedEntity in (IEnumerable)navigationEntity) - _addIncludedEntity(included, (IIdentifiable)includedEntity); + AddIncludedEntity(included, (IIdentifiable)includedEntity); else - _addIncludedEntity(included, (IIdentifiable)navigationEntity); + AddIncludedEntity(included, (IIdentifiable)navigationEntity); }); return included; } - private void _addIncludedEntity(List entities, IIdentifiable entity) + private void AddIncludedEntity(List entities, IIdentifiable entity) { - var includedEntity = _getIncludedEntity(entity); + var includedEntity = GetIncludedEntity(entity); if(includedEntity != null) entities.Add(includedEntity); } - private DocumentData _getIncludedEntity(IIdentifiable entity) + private DocumentData GetIncludedEntity(IIdentifiable entity) { if(entity == null) return null; var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(entity.GetType()); - var data = _getData(contextEntity, entity); + var data = GetData(contextEntity, entity); data.Attributes = new Dictionary(); @@ -202,13 +210,13 @@ private DocumentData _getIncludedEntity(IIdentifiable entity) return data; } - private bool _relationshipIsIncluded(string relationshipName) + private bool RelationshipIsIncluded(string relationshipName) { return _jsonApiContext.IncludedRelationships != null && _jsonApiContext.IncludedRelationships.Contains(relationshipName.ToProperCase()); } - private List> _getRelationships(IEnumerable entities, string relationshipName) + private List> GetRelationships(IEnumerable entities, string relationshipName) { var objType = entities.GetType().GenericTypeArguments[0]; @@ -224,7 +232,7 @@ private List> _getRelationships(IEnumerable e } return relationships; } - private Dictionary _getRelationship(object entity, string relationshipName) + private Dictionary GetRelationship(object entity, string relationshipName) { var objType = entity.GetType(); From 743edd13e2d907d77117f5923cabda7250f32382 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 21:51:43 -0500 Subject: [PATCH 09/14] test(sparse-fields): test that fields can be restricted using query --- .../Acceptance/Spec/SparseFieldSetTests.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs index 14e3c874a1..abddc04613 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs @@ -8,6 +8,12 @@ using JsonApiDotNetCore.Extensions; using JsonApiDotNetCoreExample.Models; using System.Linq; +using Microsoft.AspNetCore.Hosting; +using System.Net.Http; +using Microsoft.AspNetCore.TestHost; +using Newtonsoft.Json; +using JsonApiDotNetCore.Models; +using System.Diagnostics; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { @@ -46,5 +52,36 @@ public async Task Can_Select_Sparse_Fieldsets() Assert.Equal(0, result.Ordinal); Assert.Equal(todoItem.Description, result.Description); } + + [Fact] + public async Task Fields_Query_Selects_Sparse_Field_Sets() + { + // arrange + var todoItem = new TodoItem { + Description = "description", + Ordinal = 1 + }; + _dbContext.TodoItems.Add(todoItem); + await _dbContext.SaveChangesAsync(); + + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod("GET"); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var route = $"/api/v1/todo-items/{todoItem.Id}?fields[todo-items]=description"; + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + var deserializeBody = JsonConvert.DeserializeObject(body); + + // assert + Assert.Equal(todoItem.StringId, deserializeBody.Data.Id); + Assert.Equal(1, deserializeBody.Data.Attributes.Count); + Assert.Equal(todoItem.Description, deserializeBody.Data.Attributes["description"]); + } } } From 4cb667228ae5c5baf7d9ae20c5bd1576ebd09c7b Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 21 Mar 2017 07:45:48 -0500 Subject: [PATCH 10/14] docs(readme): document sparse fieldset support --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 5176708077..9ff27f5b73 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ JsonApiDotnetCore provides a framework for building [json:api](http://jsonapi.or - [Meta](#meta) - [Client Generated Ids](#client-generated-ids) - [Custom Errors](#custom-errors) + - [Sparse Fieldsets](#sparse-fieldsets) - [Tests](#tests) ## Comprehensive Demo @@ -397,6 +398,23 @@ public override async Task PostAsync([FromBody] MyEntity entity) } ``` +### Sparse Fieldsets + +We currently support top-level field selection. +What this means is you can restrict which fields are returned by a query using the `fields` query parameter, but this does not yet apply to included relationships. + +- Currently valid: +```http +GET /articles?fields[articles]=title,body HTTP/1.1 +Accept: application/vnd.api+json +``` + +- Not yet supported: +```http +GET /articles?include=author&fields[articles]=title,body&fields[people]=name HTTP/1.1 +Accept: application/vnd.api+json +``` + ## Tests I am using DotNetCoreDocs to generate sample requests and documentation. From 3d58227686f41e727cf3492afbe3ce368e8f2eb2 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 21 Mar 2017 08:09:49 -0500 Subject: [PATCH 11/14] chore(tests): move all helper classes into a helper dir --- .../{ => Helpers}/Repositories/AuthorizedTodoItemsRepository.cs | 0 .../{ => Helpers}/Services/IAuthorizationService.cs | 0 .../{ => Helpers}/Services/MetaService.cs | 0 .../{ => Helpers}/Startups/AuthorizedStartup.cs | 0 .../{ => Helpers}/Startups/ClientGeneratedIdsStartup.cs | 0 .../{ => Helpers}/Startups/MetaStartup.cs | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename test/JsonApiDotNetCoreExampleTests/{ => Helpers}/Repositories/AuthorizedTodoItemsRepository.cs (100%) rename test/JsonApiDotNetCoreExampleTests/{ => Helpers}/Services/IAuthorizationService.cs (100%) rename test/JsonApiDotNetCoreExampleTests/{ => Helpers}/Services/MetaService.cs (100%) rename test/JsonApiDotNetCoreExampleTests/{ => Helpers}/Startups/AuthorizedStartup.cs (100%) rename test/JsonApiDotNetCoreExampleTests/{ => Helpers}/Startups/ClientGeneratedIdsStartup.cs (100%) rename test/JsonApiDotNetCoreExampleTests/{ => Helpers}/Startups/MetaStartup.cs (100%) diff --git a/test/JsonApiDotNetCoreExampleTests/Repositories/AuthorizedTodoItemsRepository.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Repositories/AuthorizedTodoItemsRepository.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/Repositories/AuthorizedTodoItemsRepository.cs rename to test/JsonApiDotNetCoreExampleTests/Helpers/Repositories/AuthorizedTodoItemsRepository.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Services/IAuthorizationService.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Services/IAuthorizationService.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/Services/IAuthorizationService.cs rename to test/JsonApiDotNetCoreExampleTests/Helpers/Services/IAuthorizationService.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Services/MetaService.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Services/MetaService.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/Services/MetaService.cs rename to test/JsonApiDotNetCoreExampleTests/Helpers/Services/MetaService.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Startups/AuthorizedStartup.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/AuthorizedStartup.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/Startups/AuthorizedStartup.cs rename to test/JsonApiDotNetCoreExampleTests/Helpers/Startups/AuthorizedStartup.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Startups/ClientGeneratedIdsStartup.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/ClientGeneratedIdsStartup.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/Startups/ClientGeneratedIdsStartup.cs rename to test/JsonApiDotNetCoreExampleTests/Helpers/Startups/ClientGeneratedIdsStartup.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Startups/MetaStartup.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/MetaStartup.cs similarity index 100% rename from test/JsonApiDotNetCoreExampleTests/Startups/MetaStartup.cs rename to test/JsonApiDotNetCoreExampleTests/Helpers/Startups/MetaStartup.cs From 69afe90571f1ab7c98564c091fb35987de0854af Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 21 Mar 2017 08:10:09 -0500 Subject: [PATCH 12/14] feat(tests): add helper extension to get ef sql output --- .../Extensions/IQueryableExtensions.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/IQueryableExtensions.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/IQueryableExtensions.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/IQueryableExtensions.cs new file mode 100644 index 0000000000..a40dfb4a5a --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/IQueryableExtensions.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.Internal; +using Remotion.Linq.Parsing.Structure; +using Database = Microsoft.EntityFrameworkCore.Storage.Database; + +namespace JsonApiDotNetCoreExampleTests.Helpers.Extensions +{ + + public static class IQueryableExtensions + { + private static readonly TypeInfo QueryCompilerTypeInfo = typeof(QueryCompiler).GetTypeInfo(); + + private static readonly FieldInfo QueryCompilerField = typeof(EntityQueryProvider).GetTypeInfo().DeclaredFields.First(x => x.Name == "_queryCompiler"); + + private static readonly PropertyInfo NodeTypeProviderField = QueryCompilerTypeInfo.DeclaredProperties.Single(x => x.Name == "NodeTypeProvider"); + + private static readonly MethodInfo CreateQueryParserMethod = QueryCompilerTypeInfo.DeclaredMethods.First(x => x.Name == "CreateQueryParser"); + + private static readonly FieldInfo DataBaseField = QueryCompilerTypeInfo.DeclaredFields.Single(x => x.Name == "_database"); + + private static readonly FieldInfo QueryCompilationContextFactoryField = typeof(Database).GetTypeInfo().DeclaredFields.Single(x => x.Name == "_queryCompilationContextFactory"); + + public static string ToSql(this IQueryable query) where TEntity : class + { + if (!(query is EntityQueryable) && !(query is InternalDbSet)) + throw new ArgumentException("Invalid query"); + + var queryCompiler = (IQueryCompiler)QueryCompilerField.GetValue(query.Provider); + var nodeTypeProvider = (INodeTypeProvider)NodeTypeProviderField.GetValue(queryCompiler); + var parser = (IQueryParser)CreateQueryParserMethod.Invoke(queryCompiler, new object[] { nodeTypeProvider }); + var queryModel = parser.GetParsedQuery(query.Expression); + var database = DataBaseField.GetValue(queryCompiler); + var queryCompilationContextFactory = (IQueryCompilationContextFactory)QueryCompilationContextFactoryField.GetValue(database); + var queryCompilationContext = queryCompilationContextFactory.Create(false); + var modelVisitor = (RelationalQueryModelVisitor)queryCompilationContext.CreateQueryModelVisitor(); + modelVisitor.CreateQueryExecutor(queryModel); + var sql = modelVisitor.Queries.First().ToString(); + + return sql; + } + } +} \ No newline at end of file From ce969ed7371a731728276608569facee41d4b928 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 21 Mar 2017 08:10:26 -0500 Subject: [PATCH 13/14] feat(tests): add helper extension to normalize strings --- .../Helpers/Extensions/StringExtensions.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/StringExtensions.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/StringExtensions.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/StringExtensions.cs new file mode 100644 index 0000000000..19c7491d2a --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/StringExtensions.cs @@ -0,0 +1,15 @@ +using System.Text.RegularExpressions; + +namespace JsonApiDotNetCoreExampleTests.Helpers.Extensions +{ + + public static class StringExtensions + { + public static string Normalize(this string input) + { + return Regex.Replace(input, @"\s+", string.Empty) + .ToUpper() + .Replace('"', '\''); + } + } +} \ No newline at end of file From 0179f9e41b600d9c074a85554f9d67021776f3ed Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 21 Mar 2017 08:10:44 -0500 Subject: [PATCH 14/14] test(sparse-fields): validate the result SQL --- .../Acceptance/Spec/SparseFieldSetTests.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs index abddc04613..2b0be2dc59 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs @@ -13,7 +13,7 @@ using Microsoft.AspNetCore.TestHost; using Newtonsoft.Json; using JsonApiDotNetCore.Models; -using System.Diagnostics; +using JsonApiDotNetCoreExampleTests.Helpers.Extensions; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { @@ -40,17 +40,23 @@ public async Task Can_Select_Sparse_Fieldsets() }; _dbContext.TodoItems.Add(todoItem); await _dbContext.SaveChangesAsync(); + var expectedSql = $@"SELECT 't'.'Id', 't'.'Description' + FROM 'TodoItems' AS 't' + WHERE 't'.'Id' = {todoItem.Id}".Normalize(); // act - var result = await _dbContext + var query = _dbContext .TodoItems .Where(t=>t.Id == todoItem.Id) - .Select(fields) - .FirstAsync(); + .Select(fields); + + var resultSql = query.ToSql().Normalize(); + var result = await query.FirstAsync(); // assert Assert.Equal(0, result.Ordinal); Assert.Equal(todoItem.Description, result.Description); + Assert.Equal(expectedSql, resultSql); } [Fact]