Skip to content

Commit 7efb3c5

Browse files
authored
Merge pull request #422 from json-api-dotnet/fix/breaking-change-in-attr
undo breaking change for now
2 parents 5049281 + 75ed2de commit 7efb3c5

File tree

8 files changed

+72
-16
lines changed

8 files changed

+72
-16
lines changed

src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,9 @@ private string GetResourceNameFromDbSetProperty(PropertyInfo property, Type reso
267267
if (property.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute resourceAttribute)
268268
return resourceAttribute.ResourceName;
269269

270-
// fallback to dsherized...this should actually check for a custom IResourceNameFormatter
271-
return _resourceNameFormatter.FormatResourceName(resourceType);
270+
// fallback to the established convention using the DbSet Property.Name
271+
// e.g DbSet<FooBar> FooBars { get; set; } => "foo-bars"
272+
return _resourceNameFormatter.ApplyCasingConvention(property.Name);
272273
}
273274

274275
private (bool isJsonApiResource, Type idType) GetIdType(Type resourceType)

src/JsonApiDotNetCore/Builders/DocumentBuilder.cs

+3
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ private List<ResourceObject> IncludeRelationshipChain(
214214
{
215215
var requestedRelationship = relationshipChain[relationshipChainIndex];
216216
var relationship = parentEntity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == requestedRelationship);
217+
if(relationship == null)
218+
throw new JsonApiException(400, $"{parentEntity.EntityName} does not contain relationship {requestedRelationship}");
219+
217220
var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(parentResource, relationship.InternalRelationshipName);
218221
if (navigationEntity is IEnumerable hasManyNavigationEntity)
219222
{

src/JsonApiDotNetCore/Formatters/JsonApiReader.cs

+1-5
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,12 @@ public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
4646

4747
return InputFormatterResult.SuccessAsync(model);
4848
}
49-
catch (JsonSerializationException ex)
49+
catch (Exception ex)
5050
{
5151
_logger?.LogError(new EventId(), ex, "An error occurred while de-serializing the payload");
5252
context.ModelState.AddModelError(context.ModelName, ex, context.Metadata);
5353
return InputFormatterResult.FailureAsync();
5454
}
55-
catch (JsonApiException)
56-
{
57-
throw;
58-
}
5955
}
6056

6157
private string GetRequestBody(Stream body)

src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs

+23-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ public interface IResourceNameFormatter
2222
/// Get the publicly visible name for the given property
2323
/// </summary>
2424
string FormatPropertyName(PropertyInfo property);
25+
26+
/// <summary>
27+
/// Aoplies the desired casing convention to the internal string.
28+
/// This is generally applied to the type name after pluralization.
29+
/// </summary>
30+
string ApplyCasingConvention(string properName);
2531
}
2632

2733
public class DefaultResourceNameFormatter : IResourceNameFormatter
@@ -45,14 +51,30 @@ public string FormatResourceName(Type type)
4551
if (type.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute)
4652
return attribute.ResourceName;
4753

48-
return str.Dasherize(type.Name.Pluralize());
54+
return ApplyCasingConvention(type.Name.Pluralize());
4955
}
5056
catch (InvalidOperationException e)
5157
{
5258
throw new InvalidOperationException($"Cannot define multiple {nameof(ResourceAttribute)}s on type '{type}'.", e);
5359
}
5460
}
5561

62+
/// <summary>
63+
/// Aoplies the desired casing convention to the internal string.
64+
/// This is generally applied to the type name after pluralization.
65+
/// </summary>
66+
///
67+
/// <example>
68+
/// <code>
69+
/// _default.ApplyCasingConvention("TodoItems");
70+
/// // > "todo-items"
71+
///
72+
/// _default.ApplyCasingConvention("TodoItem");
73+
/// // > "todo-item"
74+
/// </code>
75+
/// </example>
76+
public string ApplyCasingConvention(string properName) => str.Dasherize(properName);
77+
5678
/// <summary>
5779
/// Uses the internal PropertyInfo to determine the external resource name.
5880
/// By default the name will be formatted to kebab-case.

src/JsonApiDotNetCore/Models/AttrAttribute.cs

+9-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,15 @@ public AttrAttribute(string publicName = null, bool isImmutable = false, bool is
3434
IsSortable = isSortable;
3535
}
3636

37-
internal AttrAttribute(string publicName, string internalName, bool isImmutable = false)
37+
/// <summary>
38+
/// Do not use this overload in your applications.
39+
/// Provides a method for instantiating instances of `AttrAttribute` and specifying
40+
/// the internal property name.
41+
/// The primary intent for this was to enable certain types of unit tests to be possible.
42+
/// This overload will be deprecated and removed in future releases and an alternative
43+
/// for unit tests will be provided.
44+
/// </summary>
45+
public AttrAttribute(string publicName, string internalName, bool isImmutable = false)
3846
{
3947
PublicAttributeName = publicName;
4048
InternalAttributeName = internalName;

test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
using Microsoft.EntityFrameworkCore;
1212
using Newtonsoft.Json;
1313
using Xunit;
14-
using Person = JsonApiDotNetCoreExample.Models.Person;
1514

1615
namespace JsonApiDotNetCoreExampleTests.Acceptance
1716
{
@@ -21,6 +20,7 @@ public class ManyToManyTests
2120
private static readonly Faker<Article> _articleFaker = new Faker<Article>()
2221
.RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10))
2322
.RuleFor(a => a.Author, f => new Author());
23+
2424
private static readonly Faker<Tag> _tagFaker = new Faker<Tag>().RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10));
2525

2626
private TestFixture<TestStartup> _fixture;
@@ -66,9 +66,9 @@ public async Task Can_Create_Many_To_Many()
6666
// arrange
6767
var context = _fixture.GetService<AppDbContext>();
6868
var tag = _tagFaker.Generate();
69-
var author = new Person();
69+
var author = new Author();
7070
context.Tags.Add(tag);
71-
context.People.Add(author);
71+
context.Authors.Add(author);
7272
await context.SaveChangesAsync();
7373

7474
var article = _articleFaker.Generate();
@@ -85,7 +85,7 @@ public async Task Can_Create_Many_To_Many()
8585
{ "author", new {
8686
data = new
8787
{
88-
type = "people",
88+
type = "authors",
8989
id = author.StringId
9090
}
9191
} },
@@ -111,7 +111,7 @@ public async Task Can_Create_Many_To_Many()
111111
// assert
112112
var body = await response.Content.ReadAsStringAsync();
113113
Assert.True(HttpStatusCode.Created == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}");
114-
114+
115115
var articleResponse = _fixture.GetService<IJsonApiDeSerializer>().Deserialize<Article>(body);
116116
Assert.NotNull(articleResponse);
117117

test/UnitTests/Builders/ContextGraphBuilder_Tests.cs

+2
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ public class RelatedResource : Identifiable { }
139139

140140
public class CamelCaseNameFormatter : IResourceNameFormatter
141141
{
142+
public string ApplyCasingConvention(string properName) => ToCamelCase(properName);
143+
142144
public string FormatPropertyName(PropertyInfo property) => ToCamelCase(property.Name);
143145

144146
public string FormatResourceName(Type resourceType) => ToCamelCase(resourceType.Name.Pluralize());

test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs

+26-2
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,26 @@ public void AddResourceService_Throws_If_Type_Does_Not_Implement_Any_Interfaces(
112112
Assert.Throws<JsonApiSetupException>(() => services.AddResourceService<int>());
113113
}
114114

115-
private class IntResource : Identifiable { }
116-
private class GuidResource : Identifiable<Guid> { }
115+
[Fact]
116+
public void AddJsonApi_With_Context_Uses_DbSet_PropertyName_If_NoOtherSpecified()
117+
{
118+
// arrange
119+
var services = new ServiceCollection();
120+
121+
services.AddScoped<IScopedServiceProvider, TestScopedServiceProvider>();
122+
123+
// act
124+
services.AddJsonApi<TestContext>();
125+
126+
// assert
127+
var provider = services.BuildServiceProvider();
128+
var graph = provider.GetService<IContextGraph>();
129+
var resource = graph.GetContextEntity(typeof(IntResource));
130+
Assert.Equal("resource", resource.EntityName);
131+
}
132+
133+
public class IntResource : Identifiable { }
134+
public class GuidResource : Identifiable<Guid> { }
117135

118136
private class IntResourceService : IResourceService<IntResource>
119137
{
@@ -138,5 +156,11 @@ private class GuidResourceService : IResourceService<GuidResource, Guid>
138156
public Task<GuidResource> UpdateAsync(Guid id, GuidResource entity) => throw new NotImplementedException();
139157
public Task UpdateRelationshipsAsync(Guid id, string relationshipName, List<ResourceObject> relationships) => throw new NotImplementedException();
140158
}
159+
160+
161+
public class TestContext : DbContext
162+
{
163+
public DbSet<IntResource> Resource { get; set; }
164+
}
141165
}
142166
}

0 commit comments

Comments
 (0)