|
| 1 | +# Introduction |
| 2 | +When adopting new features from recent .NET releases in OData.net, we may encounter pre-release APIs that could be useful to integrate. These pre-release versions often interface with public APIs but may not yet offer backporting options for older .NET versions. However, leveraging them through preprocessor directives is a common practice in OData.net and other .NET projects. |
| 3 | + |
| 4 | + |
| 5 | +## Example : Adding new API present only in future .NET versions |
| 6 | + |
| 7 | +```cs |
| 8 | +// Default API for lowest version that we compile for e.g. in main NET8.0 |
| 9 | +IEdmEntitySet FindEntitySet(string setName); |
| 10 | + |
| 11 | +// New API that targets a new version of .Net framework |
| 12 | +#if NET9_0_OR_GREATER |
| 13 | +IEdmEntitySet FindEntitySet(ReadOnlyMemory<char> setName); |
| 14 | +#endif |
| 15 | +``` |
| 16 | +However we also will need to start providing support for newer dotnet versions with the new release plan and in order to support these we could also adopt a few new practices for consistency. |
| 17 | + |
| 18 | +Over time, these APIs will be fully implemented using stable features in .NET 9. However, there are cases where exposing a pre-release API can provide immediate benefits, such as improving performance or enabling early adoption of upcoming .NET features. |
| 19 | + |
| 20 | +## Motivation for Using Pre-Release APIs |
| 21 | +Pre-release APIs may: |
| 22 | + |
| 23 | +* Improve performance: Offer optimizations such as reduced memory allocations. |
| 24 | +* Enhance future compatibility: Provide a migration path for upcoming .NET releases. |
| 25 | +* Enable experimentation: Allow users to try out new features before they become stable. |
| 26 | + |
| 27 | +These APIs will eventually have full implementations that leverage stable .NET 9 features. However, there are cases where exposing a new API earlier can be beneficial—both for improving our projects and for preparing for upcoming .NET framework versions that are still finalizing preview features. |
| 28 | + |
| 29 | +Preview APIs may: |
| 30 | +- Become fully supported in future stable releases. |
| 31 | +- Undergo modifications or redesigns before finalization. |
| 32 | +- Help maintain compatibility and code parity with existing stable APIs while offering immediate benefits. |
| 33 | + |
| 34 | +### **Example: Optimizing URI Parsing for Zero Allocations** |
| 35 | +Consider a scenario where we want to introduce a new API to achieve **zero allocations** while parsing a URI. |
| 36 | + |
| 37 | +#### **Current Implementation** |
| 38 | +```csharp |
| 39 | +public abstract class ODataPathSegment |
| 40 | +{ |
| 41 | + /// <summary>Returns the identifier for this segment, i.e., the string part without the keys.</summary> |
| 42 | + public string Identifier { get; set; } |
| 43 | + // ... redacted |
| 44 | +} |
| 45 | + |
| 46 | +// This references an interned string, so memory usage is optimized. |
| 47 | +this.Identifier = navigationProperty.Name; |
| 48 | + |
| 49 | +// However, we perform lookups before creating PathSegments using FindProperty, FindEntitySet, and FindSingleton in ODataUriResolver. |
| 50 | +// Currently, resolving each segment requires a Find operation or string manipulation. |
| 51 | +
|
| 52 | +// We can achieve the same functionality more efficiently by using .NET 9 Find methods, |
| 53 | +// which perform zero allocations and avoid performance penalties. |
| 54 | +``` |
| 55 | + |
| 56 | +### **Adopting Preview APIs and Migration Strategy** |
| 57 | +To adopt these new APIs efficiently, we may need to: |
| 58 | +- Replace certain existing APIs and fields with non-allocating alternatives while ensuring compatibility with stable APIs. |
| 59 | +- Target upcoming APIs that are expected to be available by the final release date. |
| 60 | +- Introduce **gated preview APIs** that users can opt into, allowing early testing before official support in .NET 9 and .NET 10. |
| 61 | + |
| 62 | +By carefully integrating these preview features, we can enhance performance while maintaining long-term support for stable .NET versions. |
| 63 | + |
| 64 | +### Migration Strategy |
| 65 | + |
| 66 | +## **Migration Strategy** |
| 67 | +### **Examples** |
| 68 | + |
| 69 | +### **Case 1: Incremental Adoption (Self-Use)** |
| 70 | +In some scenarios, users may want to introduce changes gradually, allowing services or other projects to start using them before the official release. |
| 71 | +To facilitate this, **nightly or preview builds** can be gated using the [`[Experimental]` attribute](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/experimental-attribute) introduced in .NET 8. |
| 72 | + |
| 73 | +#### **Proposal** |
| 74 | +- Introduce new features in a **non-breaking** way to maintain compatibility with existing APIs. |
| 75 | +- Ensure that any experimental API does not disrupt current functionality while allowing early adopters to test and provide feedback. |
| 76 | + |
| 77 | +### **Case 2: Stable APIs and Breaking Changes** |
| 78 | +If an API is **public**, changing its signature is a breaking change. However, we may still want to enable users on newer frameworks to benefit from improvements while maintaining support for older implementations. |
| 79 | + |
| 80 | +#### **Decision Points** |
| 81 | +- Should we provide an **alternative API** alongside the existing one? |
| 82 | +- Can we use **preprocessor directives** or conditional compilation to introduce changes only in newer framework versions? |
| 83 | +- Should we introduce an **opt-in mechanism**, such as preview attributes, to control adoption? |
| 84 | + |
| 85 | +By carefully planning the migration strategy, we can balance innovation with stability, ensuring a smooth transition for users across different .NET versions. |
| 86 | + |
| 87 | +### **Migration** |
| 88 | +#### **Case: Stable API's are used** |
| 89 | +** API is public and changing signature is breaking** but we also want to allow users using the old API in new frameworks to also benefit from the new API that we are adding which considerations should we make |
| 90 | + |
| 91 | +Do we choose |
| 92 | + |
| 93 | +**A** |
| 94 | +```cs |
| 95 | + public static IEdmTerm FindTerm(this IEdmModel model, string qualifiedName) |
| 96 | + { |
| 97 | + EdmUtil.CheckArgumentNull(model, "model"); |
| 98 | + EdmUtil.CheckArgumentNull(qualifiedName, "qualifiedName"); |
| 99 | + |
| 100 | + string fullyQualifiedName = model.ReplaceAlias(qualifiedName); |
| 101 | +#if NET9_0_OR_GREATER |
| 102 | + return FindAcrossModels( |
| 103 | + model, |
| 104 | + fullyQualifiedName.AsSpan(), |
| 105 | + findTerm, |
| 106 | + (first, second) => RegistrationHelper.CreateAmbiguousTermBinding(first, second)); |
| 107 | +#else |
| 108 | + return FindAcrossModels( // call underlying method |
| 109 | + model, |
| 110 | + fullyQualifiedName, |
| 111 | + findTerm, |
| 112 | + (first, second) => RegistrationHelper.CreateAmbiguousTermBinding(first, second)); |
| 113 | +#endif |
| 114 | + } |
| 115 | + |
| 116 | +#if NET9_0_OR_GREATER |
| 117 | + // introduce new api |
| 118 | + public static IEdmTerm FindTerm(this IEdmModel model, string qualifiedName) |
| 119 | + => FindAcrossModels( |
| 120 | + model, |
| 121 | + fullyQualifiedName, |
| 122 | + findTerm, |
| 123 | + (first, second) => RegistrationHelper.CreateAmbiguousTermBinding(first, second)); |
| 124 | +#endif |
| 125 | +``` |
| 126 | +_OR_ |
| 127 | + |
| 128 | +**B** |
| 129 | +```cs |
| 130 | + public static IEdmTerm FindTerm(this IEdmModel model, string qualifiedName) |
| 131 | + { |
| 132 | + EdmUtil.CheckArgumentNull(model, "model"); |
| 133 | + EdmUtil.CheckArgumentNull(qualifiedName, "qualifiedName"); |
| 134 | + |
| 135 | + string fullyQualifiedName = model.ReplaceAlias(qualifiedName); |
| 136 | +#if NET9_0_OR_GREATER |
| 137 | + return FindTerm( /* call new API */ |
| 138 | + model, |
| 139 | + fullyQualifiedName.AsSpan(), |
| 140 | + ); |
| 141 | +#else |
| 142 | + return FindAcrossModels( |
| 143 | + model, |
| 144 | + fullyQualifiedName, |
| 145 | + findTerm, |
| 146 | + (first, second) => RegistrationHelper.CreateAmbiguousTermBinding(first, second)); |
| 147 | +#endif |
| 148 | + } |
| 149 | + |
| 150 | +#if NET9_0_OR_GREATER |
| 151 | + // introduce new api which is stable |
| 152 | + public static IEdmTerm FindTerm(this IEdmModel model, string qualifiedName) |
| 153 | + => FindAcrossModels( |
| 154 | + model, |
| 155 | + fullyQualifiedName, |
| 156 | + findTerm, |
| 157 | + (first, second) => RegistrationHelper.CreateAmbiguousTermBinding(first, second)); |
| 158 | +#endif |
| 159 | + |
| 160 | +``` |
| 161 | + |
| 162 | +### ** Private and internal APIS ** |
| 163 | +For these we can afford to change the signatures or introduce a new parallel api during the transition period. |
| 164 | + |
| 165 | +**A** |
| 166 | + |
| 167 | +```cs |
| 168 | +#if NET9_0_OR_GREATER |
| 169 | + private static IEdmTerm FindTerm(this IEdmModel model, ReadOnlySpan<char> qualifiedName) // modifies the signature between the two |
| 170 | +#else |
| 171 | + private static IEdmTerm FindTerm(this IEdmModel model, string qualifiedName) |
| 172 | +#endif |
| 173 | + { |
| 174 | + EdmUtil.CheckArgumentNull(model, "model"); |
| 175 | + EdmUtil.CheckArgumentNull(qualifiedName, "qualifiedName"); |
| 176 | +#if NET9_0_OR_GREATER |
| 177 | + ReadOnlySpan<char> fullyQualifiedName = model.ReplaceAlias(qualifiedName); // modifies body |
| 178 | +#else |
| 179 | + string fullyQualifiedName = model.ReplaceAlias(qualifiedName); |
| 180 | +#endif |
| 181 | + return FindAcrossModels( |
| 182 | + model, |
| 183 | + fullyQualifiedName, |
| 184 | + findTerm, |
| 185 | + (first, second) => RegistrationHelper.CreateAmbiguousTermBinding(first, second)); |
| 186 | + |
| 187 | + } |
| 188 | +``` |
| 189 | + |
| 190 | +**B** |
| 191 | +```cs |
| 192 | +#if NET9_0_OR_GREATER |
| 193 | + private static IEdmTerm FindTerm(this IEdmModel model, ReadOnlySpan<char> qualifiedName) { /*Body*/} // modifies the signature between the two |
| 194 | +#else |
| 195 | + private static IEdmTerm FindTerm(this IEdmModel model, string qualifiedName) { /*Body*/} |
| 196 | +#endif |
| 197 | +``` |
| 198 | + |
| 199 | +### **Targeting an underlying api that is still in preview or releasing api in preview mode** |
| 200 | +When our codebase depends on a preview feature, we need to carefully manage its usage and communicate its status to users. |
| 201 | +Another case would be when users want to have a pre-release version with the API specification |
| 202 | + |
| 203 | +**Scenario** |
| 204 | +We may encounter cases where: |
| 205 | + |
| 206 | +* A preview API is available, and we want to use it in our project. |
| 207 | +* We want to introduce a new API that relies on a preview feature. |
| 208 | + |
| 209 | +**Considerations** |
| 210 | +* Should we mark all APIs that depend on a preview feature with [Experimental](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/experimental-attribute), or only those that are uncertain and may not be supported in the future? |
| 211 | +* If a preview feature is critical to our implementation, should we provide an alternative fallback in case it is removed? e.g. In the case of Net9 AlternateLookup if _ReadOnlySpan_ was not supported we could use the string or _ReadOnlyMemory_ api's to keep the feature before releasing the library. |
| 212 | +* Should these preview features be in a partial class that can be removed or migrated to the main class when we promote the API to GA? |
| 213 | +e.g. |
| 214 | + |
| 215 | +```cs |
| 216 | +public abstract partial class ODataPathSegment{} // stable api's inside ODataPathSegment.cs |
| 217 | +
|
| 218 | +public abstract partial class ODataPathSegment |
| 219 | +{ |
| 220 | + // api that may not be fully ready before the next stable release or is using a preview sdk. |
| 221 | +} // inside ODataPathSegment.Future|Proposed|FeatureName.cs |
| 222 | +
|
| 223 | +``` |
| 224 | + |
| 225 | +**Note**: These will be for features that may be targetting a platform when the full release is not yet out. This could be quite exciting for AspNetCoreOData. |
0 commit comments