-
Notifications
You must be signed in to change notification settings - Fork 47
Updated to support Adapter Extension #132
Changes from all commits
db83015
bf64944
666b938
f4f63c0
0488eb8
4bc534c
58cfbf6
53236bb
50609c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Text; | ||
using Microsoft.AspNetCore.JsonPatch.Internal; | ||
using Newtonsoft.Json.Serialization; | ||
|
||
namespace Microsoft.AspNetCore.JsonPatch.Adapters | ||
{ | ||
/// <summary> | ||
/// The default AdapterFactory to be used for resolving <see cref="IAdapter"/>. | ||
/// </summary> | ||
public class AdapterFactory : IAdapterFactory | ||
{ | ||
/// <inheritdoc /> | ||
public virtual IAdapter Create(object target, IContractResolver contractResolver) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about just passing in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Either way. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer passing in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it can reuse the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My words "gotten once" were poorly chosen. I meant to suggest that the line of code for getting the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just wondering how people might try and use the AdapterFactory for other purposes and having that code embedded gives them more control and makes it less likely for somebody to pass in the wrong JsonContract. I think it comes down to 6 or a half dozen, so I can go ahead and make the change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see the difference of where we're coming from. You're arguing against redundancy of the data passed to |
||
{ | ||
var jsonContract = contractResolver.ResolveContract(target.GetType()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add argument checking - what should the behavior be when the |
||
|
||
if (target is IList) | ||
{ | ||
return new ListAdapter(); | ||
} | ||
else if (jsonContract is JsonDictionaryContract jsonDictionaryContract) | ||
{ | ||
var type = typeof(DictionaryAdapter<,>).MakeGenericType(jsonDictionaryContract.DictionaryKeyType, jsonDictionaryContract.DictionaryValueType); | ||
return (IAdapter)Activator.CreateInstance(type); | ||
} | ||
else if (jsonContract is JsonDynamicContract) | ||
{ | ||
return new DynamicObjectAdapter(); | ||
} | ||
else | ||
{ | ||
return new PocoAdapter(); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be good to see some unit tests for this method now that it's an abstraction rather than a detail. |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
using Microsoft.AspNetCore.JsonPatch.Internal; | ||
using Newtonsoft.Json.Serialization; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Text; | ||
|
||
namespace Microsoft.AspNetCore.JsonPatch.Adapters | ||
{ | ||
/// <summary> | ||
/// Defines the operations used for loading an <see cref="IAdapter"/> based on the current object and ContractResolver. | ||
/// </summary> | ||
public interface IAdapterFactory | ||
{ | ||
/// <summary> | ||
/// Creates an <see cref="IAdapter"/> for the current object | ||
/// </summary> | ||
/// <param name="target">The target object</param> | ||
/// <param name="contractResolver">The current contract resolver</param> | ||
/// <returns>The needed <see cref="IAdapter"/></returns> | ||
IAdapter Create(object target, IContractResolver contractResolver); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest a |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,24 +11,43 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters | |
/// <inheritdoc /> | ||
public class ObjectAdapter : IObjectAdapterWithTest | ||
{ | ||
/// <summary> | ||
/// Initializes a new instance of <see cref="ObjectAdapter"/> using the default AdapterFactory. | ||
/// </summary> | ||
/// <param name="contractResolver">The <see cref="IContractResolver"/>.</param> | ||
/// <param name="logErrorAction">The <see cref="Action"/> for logging <see cref="JsonPatchError"/>.</param> | ||
public ObjectAdapter( | ||
IContractResolver contractResolver, | ||
Action<JsonPatchError> logErrorAction):this(contractResolver, logErrorAction, new AdapterFactory()) | ||
{ | ||
} | ||
|
||
/// <summary> | ||
/// Initializes a new instance of <see cref="ObjectAdapter"/>. | ||
/// </summary> | ||
/// <param name="contractResolver">The <see cref="IContractResolver"/>.</param> | ||
/// <param name="logErrorAction">The <see cref="Action"/> for logging <see cref="JsonPatchError"/>.</param> | ||
/// <param name="adapterFactory">The <see cref="IAdapterFactory"/> to use when creating adaptors.</param> | ||
public ObjectAdapter( | ||
IContractResolver contractResolver, | ||
Action<JsonPatchError> logErrorAction) | ||
Action<JsonPatchError> logErrorAction, | ||
IAdapterFactory adapterFactory) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is all we need - no need to modify |
||
{ | ||
ContractResolver = contractResolver ?? throw new ArgumentNullException(nameof(contractResolver)); | ||
LogErrorAction = logErrorAction; | ||
AdapterFactory = adapterFactory; | ||
} | ||
|
||
/// <summary> | ||
/// Gets or sets the <see cref="IContractResolver"/>. | ||
/// </summary> | ||
public IContractResolver ContractResolver { get; } | ||
|
||
/// <summary> | ||
/// Gets the <see cref="IAdapterFactory"/> | ||
/// </summary> | ||
public IAdapterFactory AdapterFactory { get; } | ||
|
||
/// <summary> | ||
/// Action for logging <see cref="JsonPatchError"/>. | ||
/// </summary> | ||
|
@@ -75,7 +94,7 @@ private void Add( | |
} | ||
|
||
var parsedPath = new ParsedPath(path); | ||
var visitor = new ObjectVisitor(parsedPath, ContractResolver); | ||
var visitor = new ObjectVisitor(parsedPath, ContractResolver, AdapterFactory); | ||
|
||
var target = objectToApplyTo; | ||
if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage)) | ||
|
@@ -144,7 +163,7 @@ public void Remove(Operation operation, object objectToApplyTo) | |
private void Remove(string path, object objectToApplyTo, Operation operationToReport) | ||
{ | ||
var parsedPath = new ParsedPath(path); | ||
var visitor = new ObjectVisitor(parsedPath, ContractResolver); | ||
var visitor = new ObjectVisitor(parsedPath, ContractResolver, AdapterFactory); | ||
|
||
var target = objectToApplyTo; | ||
if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage)) | ||
|
@@ -175,7 +194,7 @@ public void Replace(Operation operation, object objectToApplyTo) | |
} | ||
|
||
var parsedPath = new ParsedPath(operation.path); | ||
var visitor = new ObjectVisitor(parsedPath, ContractResolver); | ||
var visitor = new ObjectVisitor(parsedPath, ContractResolver, AdapterFactory); | ||
|
||
var target = objectToApplyTo; | ||
if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage)) | ||
|
@@ -239,7 +258,7 @@ public void Test(Operation operation, object objectToApplyTo) | |
} | ||
|
||
var parsedPath = new ParsedPath(operation.path); | ||
var visitor = new ObjectVisitor(parsedPath, ContractResolver); | ||
var visitor = new ObjectVisitor(parsedPath, ContractResolver, AdapterFactory); | ||
|
||
var target = objectToApplyTo; | ||
if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage)) | ||
|
@@ -281,7 +300,7 @@ private bool TryGetValue( | |
propertyValue = null; | ||
|
||
var parsedPath = new ParsedPath(fromLocation); | ||
var visitor = new ObjectVisitor(parsedPath, ContractResolver); | ||
var visitor = new ObjectVisitor(parsedPath, ContractResolver, AdapterFactory); | ||
|
||
var target = objectToGetValueFrom; | ||
if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,7 +51,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist | |
serializer.Populate(jObjectReader, targetOperations); | ||
|
||
// container target: the JsonPatchDocument. | ||
var container = new JsonPatchDocument(targetOperations, new DefaultContractResolver()); | ||
var container = CreateContainer(targetOperations); | ||
|
||
return container; | ||
} | ||
|
@@ -72,5 +72,14 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s | |
serializer.Serialize(writer, lst); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Create the JsonPatchDocument using the appropriate constructor | ||
/// </summary> | ||
/// <param name="operations">The operations to assign to the JsonPatchDocument</param> | ||
/// <returns>A new instance of the JsonPatchDocument</returns> | ||
protected virtual JsonPatchDocument CreateContainer(List<Operation> operations ) => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not totally sure what this is here for - how do you expect this to be used? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rynowak We added this purely for extension purposes. If I remember correctly @HappyNomad needed a custom JsonPatchDocumentConverter, and breaking out this portion to be overridden allowed the rest of the ReadJson function to be used rather than being duplicated. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, what are the scenarios for a custom converter? What problems are being solved? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking back through the thread, @HappyNomad needed to provide his custom adapter factory as part of the deserialization process. I didn't need to for my scenario, and to be honest I never went back to understand why one of us needed it and the other didn't. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @HappyNomad - care to reply? I don't think this is blocking if we don't get a response. I just like to know what's going on 😆 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For an answer, see #132 (comment) . But as long as I can pass There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggested this while thoroughly considering @jmudavid's suggestion to pass As you can see from my other comments today, I believe @jmudavid's deviation from the existing API is a mistake. Not only that, but I discovered the arguments I'd need to pass to |
||
new JsonPatchDocument(operations, new DefaultContractResolver()); | ||
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,7 +51,7 @@ public override object ReadJson( | |
serializer.Populate(jObjectReader, targetOperations); | ||
|
||
// container target: the typed JsonPatchDocument. | ||
var container = Activator.CreateInstance(objectType, targetOperations, new DefaultContractResolver()); | ||
var container = CreateTypedContainer(objectType, targetOperations); | ||
|
||
return container; | ||
} | ||
|
@@ -60,5 +60,16 @@ public override object ReadJson( | |
throw new JsonSerializationException(Resources.InvalidJsonPatchDocument, ex); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Create the JsonPatchDocument using the appropriate constructor | ||
/// </summary> | ||
/// <param name="objectType">The model type for the JsonPatchDocument</param> | ||
/// <param name="operations">The operations to assign to the JsonPatchDocument</param> | ||
/// <returns>A new instance of the JsonPatchDocument</returns> | ||
protected virtual object CreateTypedContainer(Type objectType, object operations) | ||
{ | ||
return Activator.CreateInstance(objectType, operations, new DefaultContractResolver()); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, not totally sure what this is going to be used for... it would be good to talk through the scenarios. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above. Just trying to allow custom Converters to be created with as little code duplication as possible. |
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense for the
AdapterFactory
to wrap theIContractResolver
? IE, you can provide an adapter factory or a contract resolver:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rynowak, I see how this would simplify the code by having a single element to pass around, although it seems to me like we would be asking one class to handle two different purposes. Did you have another goal in mind for consolidating these?
Late in the PR, @HappyNomad suggested we moved the AdpaterFactory to the ApplyTo, rather than being part of the Constructor. We had different philosophical views on why, but a decision there might impact a decision here. In addition to understanding the goal behind your suggestion, I wanted to also get your thoughts on that aspect. Ultimately if we can get this rolled into production it would be great, so I don't mind a little additional refactoring if it will help us move this forward, I just would rather not do it too many more times ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was asking to try and reason about whether or not they are the came concern. The current adapter factory implementation is only thing in the system that interacts with contracts current (I think).
If we were implementing this system fresh (without JSON.NET) we would put the details about how to traverse different types on our equivalent of
JsonContract
.I don't have any major concerns or strong feedback about what you and @HappyNomad have been discussing, I think both proposals are pretty reasonable. We don't have the luxury of designing something new here because it has to fit into the existing APIs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The concrete implementations of IAdapter are what actually use the ContractResolver. The AdapterFactory only uses it to pass through. You are right that we probably could pull it into the AdapterFactory, but it would create more complexity to keep the interface backwards compatible. It looks like (quick review) that the ContractResolver can be added in various ways (constructor, converter, ApplyTo, etc) so we would need to handle the old mechanism and the new mechanism in all these locations. I agree that building it fresh it probably makes sense to have the AdpaterFactory provided it if needed. I am willing to refactor if that will help us get this merged, otherwise I am inclined to make fewer changes than are needed to meet the primary goal of this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok great, I'm convinced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggested that from the beginning, although I did thoroughly consider your idea to instead accept it in
JsonPatchDocument
's constructor. I concluded to stand by my original idea to only accept it inApplyTo
. I summarized my reasoning at dotnet/aspnetcore#2816 (comment) and we discussed it starting at #132 (comment) .IAdapterFactory
is a means of customizingObjectAdapter
which is a means of customizingJsonPatchDocument
. The existing design is to passObjectAdapter
toApplyTo
. You want to allow the customization ofObjectAdapter
(IAdapterFactory
) to be passed toJsonPatchDocument
's constructor whileObjectAdapter
is still passed toApplyTo
? IMHO that's making things more messy and for no good reason.Please delegate the idea to bake custom behavior into a
JsonPatchDocument
instance to a separate issue rather than conflating it here. In that separate issue, I think you should consider aJsonPatchDocument
constructor that accepts the more generalIObjectAdapter
instead of the more specificIAdapterFactory
.Please keep this pull request focused and don't touch
JsonPatchDocument
orJsonPatchDocument<T>
. Both of our scenarios will work fine without touching those classes. The extraneous changes you suggest are unnecessary for fulfilling both of our needs. They may also benefit from further consideration in a separate issue.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's exactly my point.
IObjectAdapter
is already being passed toApplyTo
, so let's not pretend we can undo that design decision by passingIAdapterFactory
toJsonPatchDocument
's constructor. Instead, let's be consistent with the existing API and leaveJsonPatchDocument
alone.