Skip to content
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

Allow deserialization of the base 'Query' type #16388

Closed
wants to merge 3 commits into from

Conversation

gvkries
Copy link
Contributor

@gvkries gvkries commented Jul 1, 2024

If a feature with an IQuerySource that has been saved to a QueriesDocument gets deactivated, deserializing the document will fail because the base type Query cannot be deserialized.

This PR fixes this by adding a parameterless constructor to Query itself. It also ensures that any previously saved data from the queries document is preserved by using an JSON extension property.

Because I found no way to preserve the original type information when the base Query type gets re-serialized, I added an feature event handler, that fixes missing type information in the QueriesDocument when a previously used feature is reactivated.

Fixes #16372

/cc @MikeAlhayek

@MikeAlhayek
Copy link
Member

@gvkries what will happen when a feature is enabled, disable, and then enabled again?

A user will create queries (SqlQuery), then disabled the Sql feature, updates the document, then enable Sql feature. Will then lose the SqlQuery data previously provided?

@MikeAlhayek
Copy link
Member

I think adding the default constructor alone my be enough without the next for the handler. Can you please try that out?

@gvkries
Copy link
Contributor Author

gvkries commented Jul 1, 2024

No, the query should be kept intact, because the feature event handler will fix it. Basically the sequence would be:

  1. Feature is enabled and QueriesDocument contains a SqlQuery with some settings.
  2. You disable the feature and edit/create a different query afterwards (e.g. Lucene).
  3. During that editing, the SqlQuery will be deserialized as the base type Query, the original data is kept in the extension property. But when the document is saved again, the additional type information in the $type discriminator gets removed. It is now serialized as Query only.
  4. If the feature gets enabled again, the event handler will find a query of the base type with the source name Sql in the document and replace it with the derived type information.

Maybe there is a more elegant way to do this.

@MikeAlhayek
Copy link
Member

MikeAlhayek commented Jul 1, 2024

@gvkries Did you try to only add a default constructor without the event handler?

I think if the serializer is able to serialize, everything will work as expected without having to worry about fixing the document

@gvkries
Copy link
Contributor Author

gvkries commented Jul 1, 2024

@gvkries Did you try to only add a default constructor without the event handler?

I think if the serializer is able to serialize, everything will work as expected without having to worry about fixing the document

No, that is not enough. If you re-serialize a Query instance while its derived type is not available, you loose the original type information.

E.g. a original Lucene query may look lke:

"RecentBlogPosts": {
    "$type": "OrchardCore.Search.Lucene.LuceneQuery, OrchardCore.Search.Lucene",
    "Index": "Search",
    "Template": "{\r\n  \u0022query\u0022: {\r\n    \u0022term\u0022: { \u0022Content.ContentItem.ContentType\u0022: \u0022BlogPost\u0022 }\r\n  },\r\n  \u0022sort\u0022: {\r\n    \u0022Content.ContentItem.CreatedUtc\u0022: {\r\n      \u0022order\u0022: \u0022desc\u0022,\r\n      \u0022type\u0022: \u0022double\u0022\r\n    }\r\n  },\r\n  \u0022size\u0022: 3\r\n}\r\n",
    "ReturnContentItems": true,
    "Name": "RecentBlogPosts",
    "Source": "Lucene",
    "Schema": "{\r\n    \u0022type\u0022: \u0022ContentItem/BlogPost\u0022\r\n}"
}

After Lucene gets deactivated and the query is deserialized and serialized as a Query you will get this:

"RecentBlogPosts": {
    "Name": "RecentBlogPosts",
    "Source": "Lucene",
    "Schema": "{\r\n    \u0022type\u0022: \u0022ContentItem/BlogPost\u0022\r\n}",
    "Index": "Search",
    "Template": "{\r\n  \u0022query\u0022: {\r\n    \u0022term\u0022: { \u0022Content.ContentItem.ContentType\u0022: \u0022BlogPost\u0022 }\r\n  },\r\n  \u0022sort\u0022: {\r\n    \u0022Content.ContentItem.CreatedUtc\u0022: {\r\n      \u0022order\u0022: \u0022desc\u0022,\r\n      \u0022type\u0022: \u0022double\u0022\r\n    }\r\n  },\r\n  \u0022size\u0022: 3\r\n}\r\n",
    "ReturnContentItems": true
}

@MikeAlhayek
Copy link
Member

MikeAlhayek commented Jul 1, 2024

No, that is not enough. If you re-serialize a Query instance while its derived type is not available, you loose the original type information.

I think that would be enough. The data will still be in the database but won't be mapped to a concrete type. In addition to that, I would also not sure these queries on the UI to prevent someone from modifying that record until that feature is enabled again.

@MikeAlhayek
Copy link
Member

@gvkries please check out the PR #16392 which solves the same issue but without the need for the handler.

@gvkries
Copy link
Contributor Author

gvkries commented Jul 2, 2024

@gvkries please check out the PR #16392 which solves the same issue but without the need for the handler.

That is a good addition, but the handler is still required because even if the feature gets re-enabled, the QueriesDocument contains only the base Query and will not deserialize the derived type anymore. So the user would loose all queries, if we don't 'repair' the document.

@gvkries
Copy link
Contributor Author

gvkries commented Jul 2, 2024

Note the missing $type discriminator in my sample above.

Copy link
Member

@MikeAlhayek MikeAlhayek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sebastienros can you think of a better idea here to eliminate the QueriesDocumentUpdater class?

{
var document = await _documentManager.GetOrCreateMutableAsync();

if (!ValidateDocument(document) && FixupDocument(document))
Copy link
Member

@MikeAlhayek MikeAlhayek Jul 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think !ValidateDocument(document) is unnecessary and should be removed. I think using FixupDocument will be enough since we'll only iterate once instead of possible two. Also maybe call this method RepairDocument instead.

Comment on lines +52 to +54
// Check for any query that has no specific type information and try to fix it up with a derived type.
// The type information is lost if a user edits the document while it contains a query for a feature that
// has been disabled.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decorate this method with this comment instead of the body.

Comment on lines +58 to +69
if (query.GetType() == typeof(Query))
{
var sample = _querySources.FirstOrDefault(x => x.Name == query.Source)?.Create();

if (sample != null)
{
var json = JObject.FromObject(query);

document.Queries[kv.Key] = (Query)json.ToObject(sample.GetType());
hasChanged = true;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Save indentation

Suggested change
if (query.GetType() == typeof(Query))
{
var sample = _querySources.FirstOrDefault(x => x.Name == query.Source)?.Create();
if (sample != null)
{
var json = JObject.FromObject(query);
document.Queries[kv.Key] = (Query)json.ToObject(sample.GetType());
hasChanged = true;
}
}
if (query.GetType() != typeof(Query))
{
continue;
}
var sample = _querySources.FirstOrDefault(x => x.Name == query.Source)?.Create();
if (sample != null)
{
var json = JObject.FromObject(query);
document.Queries[kv.Key] = (Query)json.ToObject(sample.GetType());
hasChanged = true;
}

@sebastienros
Copy link
Member

Suggestion is to split the QueriesDocument into separate documents (one for each query) using a migration.

A mitigation to the problem could be to delete the queries when the feature is disabled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Error when disabling Lucine or elastic search feature
3 participants