diff --git a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs index 88670821fe..2eb6074b94 100644 --- a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs @@ -267,8 +267,9 @@ private string GetResourceNameFromDbSetProperty(PropertyInfo property, Type reso if (property.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute resourceAttribute) return resourceAttribute.ResourceName; - // fallback to dsherized...this should actually check for a custom IResourceNameFormatter - return _resourceNameFormatter.FormatResourceName(resourceType); + // fallback to the established convention using the DbSet Property.Name + // e.g DbSet FooBars { get; set; } => "foo-bars" + return _resourceNameFormatter.ApplyCasingConvention(property.Name); } private (bool isJsonApiResource, Type idType) GetIdType(Type resourceType) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 730dd773c4..b377dc94f5 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -214,6 +214,9 @@ private List IncludeRelationshipChain( { var requestedRelationship = relationshipChain[relationshipChainIndex]; var relationship = parentEntity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == requestedRelationship); + if(relationship == null) + throw new JsonApiException(400, $"{parentEntity.EntityName} does not contain relationship {requestedRelationship}"); + var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(parentResource, relationship.InternalRelationshipName); if (navigationEntity is IEnumerable hasManyNavigationEntity) { diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs index e10a3f31c2..3acba206fa 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs @@ -46,16 +46,12 @@ public Task ReadAsync(InputFormatterContext context) return InputFormatterResult.SuccessAsync(model); } - catch (JsonSerializationException ex) + catch (Exception ex) { _logger?.LogError(new EventId(), ex, "An error occurred while de-serializing the payload"); context.ModelState.AddModelError(context.ModelName, ex, context.Metadata); return InputFormatterResult.FailureAsync(); } - catch (JsonApiException) - { - throw; - } } private string GetRequestBody(Stream body) diff --git a/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs index 8f5bdf3bb2..6feaab949d 100644 --- a/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs +++ b/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs @@ -22,6 +22,12 @@ public interface IResourceNameFormatter /// Get the publicly visible name for the given property /// string FormatPropertyName(PropertyInfo property); + + /// + /// Aoplies the desired casing convention to the internal string. + /// This is generally applied to the type name after pluralization. + /// + string ApplyCasingConvention(string properName); } public class DefaultResourceNameFormatter : IResourceNameFormatter @@ -45,7 +51,7 @@ public string FormatResourceName(Type type) if (type.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute) return attribute.ResourceName; - return str.Dasherize(type.Name.Pluralize()); + return ApplyCasingConvention(type.Name.Pluralize()); } catch (InvalidOperationException e) { @@ -53,6 +59,22 @@ public string FormatResourceName(Type type) } } + /// + /// Aoplies the desired casing convention to the internal string. + /// This is generally applied to the type name after pluralization. + /// + /// + /// + /// + /// _default.ApplyCasingConvention("TodoItems"); + /// // > "todo-items" + /// + /// _default.ApplyCasingConvention("TodoItem"); + /// // > "todo-item" + /// + /// + public string ApplyCasingConvention(string properName) => str.Dasherize(properName); + /// /// Uses the internal PropertyInfo to determine the external resource name. /// By default the name will be formatted to kebab-case. diff --git a/src/JsonApiDotNetCore/Models/AttrAttribute.cs b/src/JsonApiDotNetCore/Models/AttrAttribute.cs index a5e594ea0c..6d48098192 100644 --- a/src/JsonApiDotNetCore/Models/AttrAttribute.cs +++ b/src/JsonApiDotNetCore/Models/AttrAttribute.cs @@ -34,7 +34,15 @@ public AttrAttribute(string publicName = null, bool isImmutable = false, bool is IsSortable = isSortable; } - internal AttrAttribute(string publicName, string internalName, bool isImmutable = false) + /// + /// Do not use this overload in your applications. + /// Provides a method for instantiating instances of `AttrAttribute` and specifying + /// the internal property name. + /// The primary intent for this was to enable certain types of unit tests to be possible. + /// This overload will be deprecated and removed in future releases and an alternative + /// for unit tests will be provided. + /// + public AttrAttribute(string publicName, string internalName, bool isImmutable = false) { PublicAttributeName = publicName; InternalAttributeName = internalName; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs index e483aa327c..77c601373b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs @@ -11,7 +11,6 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using Xunit; -using Person = JsonApiDotNetCoreExample.Models.Person; namespace JsonApiDotNetCoreExampleTests.Acceptance { @@ -21,6 +20,7 @@ public class ManyToManyTests private static readonly Faker
_articleFaker = new Faker
() .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10)) .RuleFor(a => a.Author, f => new Author()); + private static readonly Faker _tagFaker = new Faker().RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10)); private TestFixture _fixture; @@ -66,9 +66,9 @@ public async Task Can_Create_Many_To_Many() // arrange var context = _fixture.GetService(); var tag = _tagFaker.Generate(); - var author = new Person(); + var author = new Author(); context.Tags.Add(tag); - context.People.Add(author); + context.Authors.Add(author); await context.SaveChangesAsync(); var article = _articleFaker.Generate(); @@ -85,7 +85,7 @@ public async Task Can_Create_Many_To_Many() { "author", new { data = new { - type = "people", + type = "authors", id = author.StringId } } }, @@ -111,7 +111,7 @@ public async Task Can_Create_Many_To_Many() // assert var body = await response.Content.ReadAsStringAsync(); Assert.True(HttpStatusCode.Created == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}"); - + var articleResponse = _fixture.GetService().Deserialize
(body); Assert.NotNull(articleResponse); diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs index ab4412de2c..a0f54ad443 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs @@ -139,6 +139,8 @@ public class RelatedResource : Identifiable { } public class CamelCaseNameFormatter : IResourceNameFormatter { + public string ApplyCasingConvention(string properName) => ToCamelCase(properName); + public string FormatPropertyName(PropertyInfo property) => ToCamelCase(property.Name); public string FormatResourceName(Type resourceType) => ToCamelCase(resourceType.Name.Pluralize()); diff --git a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index f48821e756..946c081b62 100644 --- a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -112,8 +112,26 @@ public void AddResourceService_Throws_If_Type_Does_Not_Implement_Any_Interfaces( Assert.Throws(() => services.AddResourceService()); } - private class IntResource : Identifiable { } - private class GuidResource : Identifiable { } + [Fact] + public void AddJsonApi_With_Context_Uses_DbSet_PropertyName_If_NoOtherSpecified() + { + // arrange + var services = new ServiceCollection(); + + services.AddScoped(); + + // act + services.AddJsonApi(); + + // assert + var provider = services.BuildServiceProvider(); + var graph = provider.GetService(); + var resource = graph.GetContextEntity(typeof(IntResource)); + Assert.Equal("resource", resource.EntityName); + } + + public class IntResource : Identifiable { } + public class GuidResource : Identifiable { } private class IntResourceService : IResourceService { @@ -138,5 +156,11 @@ private class GuidResourceService : IResourceService public Task UpdateAsync(Guid id, GuidResource entity) => throw new NotImplementedException(); public Task UpdateRelationshipsAsync(Guid id, string relationshipName, List relationships) => throw new NotImplementedException(); } + + + public class TestContext : DbContext + { + public DbSet Resource { get; set; } + } } }