-
-
Notifications
You must be signed in to change notification settings - Fork 159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Parsing Response for Integration Tests #1279
Comments
I can see why including a .NET client library in JsonApiDotNetCore sounds appealing, and earlier versions actually provided that. During the migration from Newtonsoft.Json to System.Text.Json, we found that many subtleties make this non-trivial to get right. That's one of the reasons why we dropped the client library code in v5. Another reason is that the primary goal of JsonApiDotNetCore is to facilitate building a JSON:API server using ASP.NET. Our team members' time is limited, so we need to prioritize. While having built-in support for a .NET client sounds nice, we'd like to address this need using OpenAPI. OpenAPI enables the generation of a documentation website, as well as statically-typed clients in various languages using existing tools. From a testing perspective, I'd recommend a combination of the following practices:
We found that asserting at a higher level in integration tests often hides unintentional subtle breaking changes, such as sending back To summarize, our team is currently not interested in re-implementing, nor maintaining, a .NET client library. However, that shouldn't stop anyone from doing so. We'd be happy to link to your own open-source project that provides this and consider opening up internal types or adding |
Thanks for your explanation and I completely understand. Since posting the issue we've, we stopped using JsonApiDotNetCore due to external factors, but we created ended up creating a class that worked fine for parsing fine responses. I have no interest in maintaining it past this point, but perhaps our version could be a good starting point for someone else. I will share the final state of the parser here: using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries.Internal;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Serialization.Objects;
using JsonApiDotNetCore.Serialization.Request.Adapters;
using JsonApiDotNetCore.Serialization.Response;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using NodaTime;
using NodaTime.Serialization.SystemTextJson;
using System.Collections;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace JsonApiDotnetCoreParser.Tests.Tests.Controllers;
public class JsonApiDocumentParser : IDisposable
{
private readonly WebApplicationFactory<Program> _factory;
private readonly IResourceObjectAdapter _adapter;
private readonly JsonSerializerOptions _options;
private readonly IServiceScope _scope;
public JsonApiDocumentParser(WebApplicationFactory<Program> factory)
{
_factory = factory;
_scope = factory.Services.GetRequiredService<IServiceScopeFactory>().CreateScope();
_adapter = _scope.ServiceProvider.GetRequiredService<IResourceObjectAdapter>();
_options = _scope.ServiceProvider
.GetRequiredService<IJsonApiOptions>()
.SerializerReadOptions;
}
public async Task<(Document, List<T>)> ParseMultiple<T>(HttpContent content)
{
(Document? document, List<IIdentifiable> data) = await Parse<List<IIdentifiable>>(content);
List<T> castedList = data.Select(it =>
{
it.ShouldBeAssignableTo(typeof(T));
return (T)it;
})
.ToList();
return (document, castedList);
}
public async Task<(Document, T)> Parse<T>(HttpContent content)
{
(Document? document, object? data) = await Parse(content);
data.ShouldBeAssignableTo(
typeof(T),
"Potential Errors: " + JsonSerializer.Serialize(document.Errors)
);
return (document, (T)data);
}
public async Task<(Document, object?)> Parse(HttpContent content)
{
string contentAsString = await content.ReadAsStringAsync();
Document document = JsonSerializer.Deserialize<Document>(contentAsString, _options)!;
SingleOrManyData<ResourceObject> data = document.Data;
if (!data.IsAssigned || data.Value == null)
return (document, null);
List<IIdentifiable> included =
document.Included?.Select(it => ConvertedData(it)).ToList()
?? new List<IIdentifiable>();
object convertedData =
data.SingleValue != null
? ConvertedData(data.SingleValue, included)
: data.ManyValue!.Select(it => ConvertedData(it, included)).ToList();
return (document, convertedData);
}
private IIdentifiable ConvertedData(ResourceObject data, List<IIdentifiable>? includes = null)
{
RequestAdapterState state =
new(new JsonApiRequest(), new TargetedFields())
{
WritableTargetedFields = new TargetedFields()
};
AssignRelationships(data);
(IIdentifiable resource, ResourceType resourceType) = _adapter.Convert(
data,
new ResourceIdentityRequirements(),
state
);
if (includes == null)
return resource;
foreach (RelationshipAttribute relationship in resourceType.Relationships)
{
var propertyValue = relationship.Property.GetValue(resource);
if (propertyValue == null)
continue;
if (relationship is HasManyAttribute)
{
IEnumerable<IIdentifiable> identifiables = (
(IEnumerable)propertyValue
).Cast<IIdentifiable>();
HashSet<IIdentifiable> found = identifiables
.Select(
identifiable =>
includes
.Where(it => relationship.RightType.ClrType == it.GetType())
.FirstOrDefault(it => it.StringId == identifiable.StringId)
)
.Where(it => it != null)
.Select(it => it!)
.ToHashSet();
Type genericArgument = propertyValue.GetType().GetGenericArguments()[0];
MethodInfo cast = typeof(Enumerable)
.GetMethod(nameof(Enumerable.Cast))!
.MakeGenericMethod(genericArgument);
var f = typeof(IEnumerable<>).MakeGenericType(genericArgument);
MethodInfo toHashSet = typeof(Enumerable)
.GetMethods()
.First(it => it.Name == nameof(Enumerable.ToHashSet))!
.MakeGenericMethod(genericArgument);
var castedEnumerable = cast.Invoke(found, new[] { found });
var castedHashSet = toHashSet.Invoke(castedEnumerable, new[] { castedEnumerable });
relationship.Property.SetValue(resource, castedHashSet);
}
else
{
IIdentifiable? identifiable = (IIdentifiable?)propertyValue;
if (identifiable == null)
continue;
IIdentifiable? found = includes
.Where(it => relationship.RightType.ClrType == it.GetType())
.FirstOrDefault(it => it.StringId == identifiable.StringId);
if (found == null)
continue;
relationship.Property.SetValue(resource, found);
}
}
return resource;
}
private ResponseModelAdapter CreateAdapter<TResource>(string? primaryId)
where TResource : class, IIdentifiable
{
IResourceGraph resourceGraph = _factory.Services.GetRequiredService<IResourceGraph>();
JsonApiRequest request =
new()
{
Kind = EndpointKind.Primary,
PrimaryResourceType = resourceGraph.GetResourceType<TResource>(),
PrimaryId = primaryId,
WriteOperation = WriteOperationKind.CreateResource
};
return new ResponseModelAdapter(
request,
_factory.Services.GetRequiredService<IJsonApiOptions>(),
new NoLinksLinkBuilder(),
_scope.ServiceProvider.GetRequiredService<IMetaBuilder>(),
_scope.ServiceProvider.GetRequiredService<IResourceDefinitionAccessor>(),
_scope.ServiceProvider.GetRequiredService<IEvaluatedIncludeCache>(),
_scope.ServiceProvider.GetRequiredService<ISparseFieldSetCache>(),
new EmptyRequestQueryStringAccessor()
);
}
private static void AssignRelationships(ResourceObject data)
{
if (data.Relationships == null)
return;
foreach ((string? _, RelationshipObject? value) in data.Relationships)
{
if (value == null)
continue;
value.Data = new SingleOrManyData<ResourceIdentifierObject>(value.Data.Value);
}
}
public void Dispose()
{
_scope.Dispose();
}
} You'll need two auxiliary classes for the adapter as well: using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Serialization.Objects;
using JsonApiDotNetCore.Serialization.Response;
namespace JsonApiDotnetCoreParser.Tests.Tests.Controllers;
internal sealed class NoLinksLinkBuilder : ILinkBuilder
{
public TopLevelLinks? GetTopLevelLinks() => null;
public ResourceLinks? GetResourceLinks(ResourceType resourceType, IIdentifiable resource) =>
null;
public RelationshipLinks? GetRelationshipLinks(
RelationshipAttribute relationship,
IIdentifiable leftResource
) => null;
} and using JsonApiDotNetCore.QueryStrings;
using Microsoft.AspNetCore.Http;
namespace JsonApiDotnetCoreParser.Tests.Tests.Controllers;
internal sealed class EmptyRequestQueryStringAccessor : IRequestQueryStringAccessor
{
public IQueryCollection Query { get; } = new QueryCollection();
} |
Thanks for sharing. This is great for future reference. I wish you the best with your project. If you need anything from us, please don't hesitate to reach out. |
Is your feature request related to a problem? Please describe.
When writing integration tests for our project, we need to deserialize the response from requests into the actual classes from the project. This allows us to verify if the program returned the correct data by comparing the returned instance with an expected one. Currently, we're using a workaround to parse the results, but this has its limitations, specifically when it comes to relationships.
Describe the solution you'd like
We're looking for a more robust solution for parsing the response from a request in our integration tests. Ideally, this solution should be able to handle relationships and other complexities in the response. It would be great if there were a built-in method or feature in JsonApiDotNetCore that could facilitate this.
Describe alternatives you've considered
Currently, we're using a custom JsonApiDocumentParser class to parse the response. This class uses the IResourceObjectAdapter and IJsonApiOptions services to convert the response data into our project's classes. However, this solution is not ideal as it feels like a "hack" and has limitations. We also tried other client libraries such as
JsonApiFramework.Client
, but this resulted in a lot of setup.Additional context
Here's our current solution:
The text was updated successfully, but these errors were encountered: