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

Refactor annotation code generation #21329

Merged
merged 1 commit into from
Jun 20, 2020
Merged

Refactor annotation code generation #21329

merged 1 commit into from
Jun 20, 2020

Conversation

roji
Copy link
Member

@roji roji commented Jun 18, 2020

  • IAnnotationCodeGenerator now exposes an API to generate all fluent API calls, not one-by-one as before. This allows generating multiple annotations as a single fluent API call.
  • Use this in SQL Server to properly generate a fluent API for identity (increment/seed) and HiLo.
  • The AnnotationCodeGenerator base class exposes the same one-by-one API as before, so this is totally backwards compatible (unless the interface is implemented directly).
  • Use IAnnotationCodeGenerator when generating the model snapshot, to have proper fluent API calls instead of raw annotations. This also DRYs lots of logic between CSharpSnapshotGenerator and CSharpDbContextGenerator.
  • IAnnotationCodeGenerator centralizes ignoring annotations that shouldn't be generated in code (also DRY between CSharpSnapshotGenerator and CSharpDbContextGenerator, Remove extra model annotations from snapshot #15675).
  • This is also a step towards helping non-C# language support, since it moves generic logic out of C#-specific types to AnnotationCodeGenerator, which isn't.
  • For now, I've only moved logic from CSharpDbContextGenerator/CSharpSnapshotGenerator to AnnotationCodeGenerator where annotations are translated into a fluent API/data annotation. We could consider also moving other cases - e.g. IsConcurrencyToken which isn't annotation-based - but that means AnnotationCodeGenerator becomes something like a general CodeGenerationHelper.

Notes

  • AnnotationCodeGenerator currently references CoreAnnotationNames which is internal (warning is suppressed). In Design we weren't enforcing the analyzer warning, but this is in Relational... We should consider making annotation names public.
  • Annotations with null values were ignored in various places, I centralized this in AnnotationCodeGenerator.RemoveIgnoredAnnotations. I hope that's correct.
  • Should we always generate fluent API calls with all defaults explicitly, to bake in the values (e.g. pass seed/increment 1 in UseIdentityColumn)? The more we bake in, the better, it seems.
  • This doesn't currently support scaffolding data annotations on keys/foreign keys/indexes (e.g. a future SQL Server [Clustered] attribute). This shouldn't be too hard, but I propose scoping it out until we decide to actually implement one.
  • The annotation ignore list is currently global and not per-type (model, entity, property...) - that's the way it already was in MigrationsCodeGenerator. I think it should be fine, just sayin'.

Supercedes #21210

@AndriySvyryd
Copy link
Member

AndriySvyryd commented Jun 19, 2020

AnnotationCodeGenerator currently references CoreAnnotationNames which is internal (warning is suppressed). In Design we weren't enforcing the analyzer warning, but this is in Relational... We should consider making annotation names public.

Core annotations are an implementation detail and shouldn't be treated as other annotations, instead the configuration should only be accessed through public API. In the future they won't be returned together with other annotations - #19806. So it might make sense to add them to the ignore list pre-emptively.

It's ok to depend on CoreAnnotationNames.AllNames just for the purpose of filtering them out.

@roji
Copy link
Member Author

roji commented Jun 19, 2020

Core annotations are an implementation detail and shouldn't be treated as other annotations, instead the configuration should only be accessed through public API. In the future they won't be returned together with other annotations - #19806. So it might make sense to add them to the ignore list pre-emptively.

I guess that mean that annotation conventions (e.g. PropertyAnnotationChanged) wouldn't be fired for them, requiring new conventions for facets? Would we also do this for relational annotations (why not)? Am curious what actual problems this extra complexity is meant to solve...

In any case, it's going to be a problem for the current approach in this PR:

  • This currently gradually filters out annotations as they are generated or ignored. For example, if data annotations are on, CSharpDbContextGenerator simply runs the annotations through the code generator's GenerateDataAnnotation, to get rid of annotations that would be expressed via attributes - what's left is fluent API. If all core annotations are pre-emptively removed, this filtering approach doesn't work.
  • Similarly, a certain MaxLength may be by-convention for a certain type in a certain provider, and it's very easy to do that right now in the code generator (RemoveConventionalAnnotations). If MaxLength becomes something else that isn't an annotation, this doesn't work.

There's ways around this (pass dataAnnotations and withConventional flags to the generator to control what is generated), but in general there is something very useful in treating configuration data via a single data model (annotations), for consumers that treat need to generally handle configuration.

Anyway, let me know and if this is the way we're going, I'll change this PR accordingly.

@AndriySvyryd
Copy link
Member

AndriySvyryd commented Jun 19, 2020

I guess that mean that annotation conventions (e.g. PropertyAnnotationChanged) wouldn't be fired for them, requiring new conventions for facets?

That's already the case

Would we also do this for relational annotations (why not)?

With #19806 *AnnotationChanged shouldn't be fired for any known annotation, however the relational annotations will always be annotations.

Am curious what actual problems this extra complexity is meant to solve...

The core model might have other implementations (like compiled model) that choose to represent those facets without using annotations for perf.

In any case, it's going to be a problem for the current approach in this PR:

  • This currently gradually filters out annotations as they are generated or ignored. For example, if data annotations are on, CSharpDbContextGenerator simply runs the annotations through the code generator's GenerateDataAnnotation, to get rid of annotations that would be expressed via attributes - what's left is fluent API. If all core annotations are pre-emptively removed, this filtering approach doesn't work.
  • Similarly, a certain MaxLength may be by-convention for a certain type in a certain provider, and it's very easy to do that right now in the code generator (RemoveConventionalAnnotations). If MaxLength becomes something else that isn't an annotation, this doesn't work.
    There's ways around this (pass dataAnnotations and withConventional flags to the generator to control what is generated), but in general there is something very useful in treating configuration data via a single data model (annotations), for consumers that treat need to generally handle configuration.

Just treat them as any other core facet, this shouldn't require changing your design.

I appreciate that having a uniform abstraction makes the code better. But that's not what the annotations were designed for, they are meant for extending the core model.

/// </summary>
/// <param name="annotations"> The annotations to remove from. </param>
/// <param name="annotationNames"> The ignored annotation names. </param>
protected virtual void IgnoreAnnotations(
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this a breaking change?

Copy link
Member Author

Choose a reason for hiding this comment

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

There are several in this PR, since the actual design is changing quite a bit. But this is in the design assembly, where we usually have lower standards. If we feel like we need to maintain backwards compat here somehow I can look into bringing it back.

Copy link
Member

Choose a reason for hiding this comment

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

Then it contradicts

The AnnotationCodeGenerator base class exposes the same one-by-one API as before, so this is totally backwards compatible (unless the interface is implemented directly).

I am fine either if we want to break/be back-compat/obsolete but it should be consistent.

Copy link
Member Author

@roji roji Jun 19, 2020

Choose a reason for hiding this comment

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

I think breaking changes in Design aren't the same as in explicitly provider-facing types such as AnnotationCodeGenerator.

/// <param name="property"> The <see cref="IProperty" />. </param>
/// <param name="annotation"> The <see cref="IAnnotation" />. </param>
/// <returns> <see langword="null" />. </returns>
public virtual AttributeCodeFragment GenerateDataAnnotation([NotNull] IProperty property, [NotNull] IAnnotation annotation)
Copy link
Member

Choose a reason for hiding this comment

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

Why is this public?

Copy link
Member Author

Choose a reason for hiding this comment

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

The idea is to allow providers to generate arbitrary data annotation attributes for annotations - this is the hook for that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah sorry, you mean why not protected. Yeah, this and all the previously public GenerateFluentApi should be protected - though that would be a small breaking change for the latter (which I think we should do).

Copy link
Member

Choose a reason for hiding this comment

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

I mean why are we adding API for individual annotation when we are moving towards framework of all annotations together.

Copy link
Member Author

Choose a reason for hiding this comment

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

Most annotations still have a single fluent API (or data annotation), so this is a trivial way for providers to implement that. I'd only expect providers to override the multi-annotation API (GenerateFluentApiCalls) when multiple annotations need to go into the same fluent API call, as with identity increment/seed. This also preserves compatibility with the existing API to a large extent.

@roji
Copy link
Member Author

roji commented Jun 19, 2020

Just treat them as any other core facet, this shouldn't require changing your design.

The design need to change because the core annotations no longer appear in the the dictionary I'm passing around, right? (because they've already been removed by RemoveIgnoredAnnotation, or after #19806, not even returned from GetAnnotations). So there needs to be a different mechanism for knowing whether a given annotation has already generated as a data annotation, etc.?

(not pushing back on anything, just making sure we're aligned)

@AndriySvyryd
Copy link
Member

The design need to change because the core annotations no longer appear in the the dictionary I'm passing around, right? (because they've already been removed by RemoveIgnoredAnnotation, or after #19806, not even returned from GetAnnotations). So there needs to be a different mechanism for knowing whether a given annotation has already generated as a data annotation, etc.?

(not pushing back on anything, just making sure we're aligned)

That's correct, but you are already handling other facets that were never stored as annotations (e.g. .IsRequired() / [Required]), so that mechanism is in place.

@roji
Copy link
Member Author

roji commented Jun 19, 2020

That's correct, but you are already handling other facets that were never stored as annotations (e.g. .IsRequired() / [Required]), so that mechanism is in place.

Am I? The idea for now was only to handle annotations. But if we want to move non-annotation handling into this as well (especially since core annotations aren't really annotations), we can do that. In that case the type should probably be renamed to something like CodeGenerationHelper - it seems like we're breaking it anyway?

@AndriySvyryd
Copy link
Member

Am I? The idea for now was only to handle annotations. But if we want to move non-annotation handling into this as well (especially since core annotations aren't really annotations), we can do that. In that case the type should probably be renamed to something like CodeGenerationHelper - it seems like we're breaking it anyway?

Yes, it's handled in CSharpDbContextGenerator. If the idea for AnnotationCodeGenerator is to only handle annotations and the core annotations aren't part of this then it shouldn't handle them unless we do what you propose and handle all facets there.

@roji
Copy link
Member Author

roji commented Jun 19, 2020

If the idea for AnnotationCodeGenerator is to only handle annotations and the core annotations aren't part of this then it shouldn't handle them unless we do what you propose and handle all facets there.

That was the original idea, yes (see note in the original post). But I'm fine with going further and moving non-annotation-based code generation into this as well, should we do this? In that case, do we rename to CodeGenerationHelper or similar? Note that code the gets generated differently between scaffolding and model snapshot would still stay in the respective generators.

@AndriySvyryd
Copy link
Member

That was the original idea, yes (see note in the original post). But I'm fine with going further and moving non-annotation-based code generation into this as well, should we do this? In that case, do we rename to CodeGenerationHelper or similar? Note that code the gets generated differently between scaffolding and model snapshot would still stay in the respective generators.

I don't think there's enough benefit in doing this

@roji
Copy link
Member Author

roji commented Jun 19, 2020

OK, I will leave core annotations and facet handling out of this type entirely, and handle only relational/provider-specific.

@roji roji requested a review from AndriySvyryd June 20, 2020 13:11
@roji roji force-pushed the AnnotationCodeGeneration branch from 527aee2 to 96becc2 Compare June 20, 2020 20:15
@roji roji force-pushed the AnnotationCodeGeneration branch from 96becc2 to c4a2bad Compare June 20, 2020 20:23
@roji roji merged commit 61f3c0b into master Jun 20, 2020
@roji roji deleted the AnnotationCodeGeneration branch June 20, 2020 21:17
@AndriySvyryd
Copy link
Member

Another thing: make sure that ProductVersion is still being added to the snapshot, add a test if it's not.

roji added a commit that referenced this pull request Jun 21, 2020
@roji
Copy link
Member Author

roji commented Jun 21, 2020

@AndriySvyryd good catch, opened #21360.

roji added a commit that referenced this pull request Jun 22, 2020
Copy link
Contributor

@bricelam bricelam left a comment

Choose a reason for hiding this comment

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

Good stuff

/// <summary>
/// The type mapper.
/// </summary>
public IRelationalTypeMappingSource RelationalTypeMappingSource { get; }
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing With?

Copy link
Member Author

Choose a reason for hiding this comment

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

Am surprised the API consistency tests didn't catch this. Will fix and look into the tests too.

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

Successfully merging this pull request may close these issues.

4 participants