Skip to content

Commit 4e6765a

Browse files
committed
Replace [Obsolete] with [Experimental] attributes (#1564)
* Replaced Obsolete with Experimental attributes throughout solution, added documentation and updated unit tests Signed-off-by: Whit Waldo <[email protected]>
1 parent 1737496 commit 4e6765a

22 files changed

+265
-122
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
---
2+
type: docs
3+
title: "Dapr .NET SDK Development with Dapr CLI"
4+
linkTitle: "Experimental Attributes"
5+
weight: 61000
6+
description: Learn about local development with the Dapr CLI
7+
---
8+
9+
## Experimental Attributes
10+
11+
### Introduction to Experimental Attributes
12+
13+
With the release of .NET 8, C# 12 introduced the `[Experimental]` attribute, which provides a standardized way to mark
14+
APIs that are still in development or experimental. This attribute is defined in the `System.Diagnostics.CodeAnalysis`
15+
namespace and requires a diagnostic ID parameter used to generate compiler warnings when the experimental API
16+
is used.
17+
18+
In the Dapr .NET SDK, we now use the `[Experimental]` attribute instead of `[Obsolete]` to mark building blocks and
19+
components that have not yet passed the stable lifecycle certification. This approach provides a clearer distinction
20+
between:
21+
22+
1. **Experimental APIs** - Features that are available but still evolving and have not yet been certified as stable
23+
according to the [Dapr Component Certification Lifecycle](https://docs.dapr.io/operations/components/certification-lifecycle/).
24+
25+
2. **Obsolete APIs** - Features that are truly deprecated and will be removed in a future release.
26+
27+
### Usage in the Dapr .NET SDK
28+
29+
In the Dapr .NET SDK, we apply the `[Experimental]` attribute at the class level for building blocks that are still in
30+
the Alpha or Beta stages of the [Component Certification Lifecycle](https://docs.dapr.io/operations/components/certification-lifecycle/).
31+
The attribute includes:
32+
33+
- A diagnostic ID that identifies the experimental building block
34+
- A URL that points to the relevant documentation for that block
35+
36+
For example:
37+
38+
```csharp
39+
using System.Diagnostics.CodeAnalysis;
40+
namespace Dapr.Cryptography.Encryption
41+
{
42+
[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")]
43+
public class DaprEncryptionClient
44+
{
45+
// Implementation
46+
}
47+
}
48+
```
49+
50+
The diagnostic IDs follow a naming convention of `DAPR_[BUILDING_BLOCK_NAME]`, such as:
51+
52+
- `DAPR_CONVERSATION` - For the Conversation building block
53+
- `DAPR_CRYPTOGRAPHY` - For the Cryptography building block
54+
- `DAPR_JOBS` - For the Jobs building block
55+
- `DAPR_DISTRIBUTEDLOCK` - For the Distributed Lock building block
56+
57+
### Suppressing Experimental Warnings
58+
59+
When you use APIs marked with the `[Experimental]` attribute, the compiler will generate errors.
60+
To build your solution without marking your own code as experimental, you will need to suppress these errors. Here are
61+
several approaches to do this:
62+
63+
#### Option 1: Using #pragma directive
64+
65+
You can use the `#pragma warning` directive to suppress the warning for specific sections of code:
66+
67+
```csharp
68+
// Disable experimental warning
69+
#pragma warning disable DAPR_CRYPTOGRAPHY
70+
// Your code using the experimental API
71+
var client = new DaprEncryptionClient();
72+
// Re-enable the warning
73+
#pragma warning restore DAPR_CRYPTOGRAPHY
74+
```
75+
76+
This approach is useful when you want to suppress warnings only for specific sections of your code.
77+
78+
#### Option 2: Project-level suppression
79+
80+
To suppress warnings for an entire project, add the following to your `.csproj` file.
81+
file.
82+
83+
```xml
84+
<PropertyGroup>
85+
<NoWarn>$(NoWarn);DAPR_CRYPTOGRAPHY</NoWarn>
86+
</PropertyGroup>
87+
```
88+
89+
You can include multiple diagnostic IDs separated by semicolons:
90+
91+
```xml
92+
<PropertyGroup>
93+
<NoWarn>$(NoWarn);DAPR_CONVERSATION;DAPR_JOBS;DAPR_DISTRIBUTEDLOCK;DAPR_CRYPTOGRAPHY</NoWarn>
94+
</PropertyGroup>
95+
```
96+
97+
This approach is particularly useful for test projects that need to use experimental APIs.
98+
99+
#### Option 3: Directory-level suppression
100+
101+
For suppressing warnings across multiple projects in a directory, add a `Directory.Build.props` file:
102+
103+
```xml
104+
<PropertyGroup>
105+
<NoWarn>$(NoWarn);DAPR_CONVERSATION;DAPR_JOBS;DAPR_DISTRIBUTEDLOCK;DAPR_CRYPTOGRAPHY</NoWarn>
106+
</PropertyGroup>
107+
```
108+
109+
This file should be placed in the root directory of your test projects. You can learn more about using
110+
`Directory.Build.props` files in the
111+
[MSBuild documentation](https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory).
112+
113+
### Lifecycle of Experimental APIs
114+
115+
As building blocks move through the certification lifecycle and reach the "Stable" stage, the `[Experimental]` attribute will be removed. No migration or code changes will be required from users when this happens, except for the removal of any warning suppressions if they were added.
116+
117+
Conversely, the `[Obsolete]` attribute will now be reserved exclusively for APIs that are truly deprecated and scheduled for removal. When you see a method or class marked with `[Obsolete]`, you should plan to migrate away from it according to the migration guidance provided in the attribute message.
118+
119+
### Best Practices
120+
121+
1. **In application code:**
122+
- Be cautious when using experimental APIs, as they may change in future releases
123+
- Consider isolating usage of experimental APIs to make future updates easier
124+
- Document your use of experimental APIs for team awareness
125+
126+
2. **In test code:**
127+
- Use project-level suppression to avoid cluttering test code with warning suppressions
128+
- Regularly review which experimental APIs you're using and check if they've been stabilized
129+
130+
3. **When contributing to the SDK:**
131+
- Use `[Experimental]` for new building blocks that haven't completed certification
132+
- Use `[Obsolete]` only for truly deprecated APIs
133+
- Provide clear documentation links in the `UrlFormat` parameter
134+
135+
### Additional Resources
136+
137+
- [Dapr Component Certification Lifecycle](https://docs.dapr.io/operations/components/certification-lifecycle/)
138+
- [C# Experimental Attribute Documentation](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/experimental-attribute)
Lines changed: 31 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Diagnostics.CodeAnalysis;
23
using System.Linq;
34
using System.Text.Json;
45
using System.Threading.Tasks;
@@ -11,24 +12,15 @@
1112
namespace DistributedLock.Controllers;
1213

1314
[ApiController]
14-
public class BindingController : ControllerBase
15+
[Experimental("DAPR_DISTRIBUTEDLOCK", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/distributed-lock/distributed-lock-api-overview/")]
16+
public class BindingController(DaprClient client, ILogger<BindingController> logger) : ControllerBase
1517
{
16-
private DaprClient client;
17-
private ILogger<BindingController> logger;
18-
private string appId;
19-
20-
public BindingController(DaprClient client, ILogger<BindingController> logger)
21-
{
22-
this.client = client;
23-
this.logger = logger;
24-
this.appId = Environment.GetEnvironmentVariable("APP_ID");
25-
}
26-
18+
private string appId = Environment.GetEnvironmentVariable("APP_ID");
19+
2720
[HttpPost("cronbinding")]
28-
[Obsolete]
2921
public async Task<IActionResult> HandleBindingEvent()
3022
{
31-
logger.LogInformation($"Received binding event on {appId}, scanning for work.");
23+
logger.LogInformation("Received binding event on {appId}, scanning for work.", appId);
3224

3325
var request = new BindingRequest("localstorage", "list");
3426
var result = client.InvokeBindingAsync(request);
@@ -47,44 +39,40 @@ public async Task<IActionResult> HandleBindingEvent()
4739
return Ok();
4840
}
4941

50-
51-
[Obsolete]
5242
private async Task AttemptToProcessFile(string fileName)
5343
{
5444
// Locks are Disposable and will automatically unlock at the end of a 'using' statement.
55-
logger.LogInformation($"Attempting to lock: {fileName}");
56-
await using (var fileLock = await client.Lock("redislock", fileName, appId, 60))
45+
logger.LogInformation("Attempting to lock: {fileName}", fileName);
46+
await using var fileLock = await client.Lock("redislock", fileName, appId, 60);
47+
if (fileLock.Success)
5748
{
58-
if (fileLock.Success)
59-
{
60-
logger.LogInformation($"Successfully locked file: {fileName}");
49+
logger.LogInformation("Successfully locked file: {fileName}", fileName);
6150

62-
// Get the file after we've locked it, we're safe here because of the lock.
63-
var fileState = await GetFile(fileName);
51+
// Get the file after we've locked it, we're safe here because of the lock.
52+
var fileState = await GetFile(fileName);
6453

65-
if (fileState == null)
66-
{
67-
logger.LogWarning($"File {fileName} has already been processed!");
68-
return;
69-
}
54+
if (fileState == null)
55+
{
56+
logger.LogWarning("File {fileName} has already been processed!", fileName);
57+
return;
58+
}
7059

71-
// "Analyze" the file before committing it to our remote storage.
72-
fileState.Analysis = fileState.Number > 50 ? "High" : "Low";
60+
// "Analyze" the file before committing it to our remote storage.
61+
fileState.Analysis = fileState.Number > 50 ? "High" : "Low";
7362

74-
// Save it to remote storage.
75-
await client.SaveStateAsync("redisstore", fileName, fileState);
63+
// Save it to remote storage.
64+
await client.SaveStateAsync("redisstore", fileName, fileState);
7665

77-
// Remove it from local storage.
78-
var bindingDeleteRequest = new BindingRequest("localstorage", "delete");
79-
bindingDeleteRequest.Metadata["fileName"] = fileName;
80-
await client.InvokeBindingAsync(bindingDeleteRequest);
66+
// Remove it from local storage.
67+
var bindingDeleteRequest = new BindingRequest("localstorage", "delete");
68+
bindingDeleteRequest.Metadata["fileName"] = fileName;
69+
await client.InvokeBindingAsync(bindingDeleteRequest);
8170

82-
logger.LogInformation($"Done processing {fileName}");
83-
}
84-
else
85-
{
86-
logger.LogWarning($"Failed to lock {fileName}.");
87-
}
71+
logger.LogInformation("Done processing {fileName}", fileName);
72+
}
73+
else
74+
{
75+
logger.LogWarning("Failed to lock {fileName}.", fileName);
8876
}
8977
}
9078

@@ -103,4 +91,4 @@ private async Task<StateData> GetFile(string fileName)
10391
return null;
10492
}
10593
}
106-
}
94+
}

examples/Directory.Build.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
<OutputPath>$(RepoRoot)bin\$(Configuration)\examples\$(MSBuildProjectName)\</OutputPath>
88

99
<IsPackable>false</IsPackable>
10+
<NoWarn>$(NoWarn);DAPR_CONVERSATION;DAPR_JOBS;DAPR_DISTRIBUTEDLOCK;DAPR_CRYPTOGRAPHY</NoWarn>
1011
</PropertyGroup>
1112
</Project>

src/Dapr.AI/Conversation/DaprConversationClient.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// limitations under the License.
1212
// ------------------------------------------------------------------------
1313

14+
using System.Diagnostics.CodeAnalysis;
1415
using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr;
1516

1617
namespace Dapr.AI.Conversation;
@@ -30,6 +31,7 @@ namespace Dapr.AI.Conversation;
3031
/// exhaustion and other problems.
3132
/// </para>
3233
/// </summary>
34+
[Experimental("DAPR_CONVERSATION", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/conversation/conversation-overview/")]
3335
public abstract class DaprConversationClient : DaprAIClient
3436
{
3537
/// <summary>

src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// limitations under the License.
1212
// ------------------------------------------------------------------------
1313

14+
using System.Diagnostics.CodeAnalysis;
1415
using Dapr.Common;
1516
using Microsoft.Extensions.Configuration;
1617
using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr;
@@ -21,6 +22,7 @@ namespace Dapr.AI.Conversation;
2122
/// Used to create a new instance of a <see cref="DaprConversationClient"/>.
2223
/// </summary>
2324
/// <param name="configuration">An optional <see cref="IConfiguration"/> to configure the client with.</param>
25+
[Experimental("DAPR_CONVERSATION", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/conversation/conversation-overview/")]
2426
public sealed class DaprConversationClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder<DaprConversationClient>(configuration)
2527
{
2628
/// <summary>
@@ -30,6 +32,7 @@ public sealed class DaprConversationClientBuilder(IConfiguration? configuration
3032
/// <summary>
3133
/// Builds the client instance from the properties of the builder.
3234
/// </summary>
35+
[Experimental("DAPR_CONVERSATION", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/conversation/conversation-overview/")]
3336
public override DaprConversationClient Build()
3437
{
3538
var daprClientDependencies = BuildDaprClientDependencies(typeof(DaprConversationClient).Assembly);

src/Dapr.AI/Conversation/DaprConversationGrpcClient.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// limitations under the License.
1212
// ------------------------------------------------------------------------
1313

14+
using System.Diagnostics.CodeAnalysis;
1415
using Dapr.Common;
1516
using Dapr.Common.Extensions;
1617
using Autogenerated = Dapr.Client.Autogen.Grpc.v1;
@@ -23,6 +24,7 @@ namespace Dapr.AI.Conversation;
2324
/// <param name="client">The Dapr client.</param>
2425
/// <param name="httpClient">The HTTP client used by the client for calling the Dapr runtime.</param>
2526
/// <param name="daprApiToken">An optional token required to send requests to the Dapr sidecar.</param>
27+
[Experimental("DAPR_CONVERSATION", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/conversation/conversation-overview/")]
2628
internal sealed class DaprConversationGrpcClient(Autogenerated.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : DaprConversationClient(client, httpClient, daprApiToken: daprApiToken)
2729
{
2830
/// <summary>

src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// limitations under the License.
1212
// ------------------------------------------------------------------------
1313

14+
using System.Diagnostics.CodeAnalysis;
1415
using Dapr.Common.Extensions;
1516
using Microsoft.Extensions.DependencyInjection;
1617

@@ -24,6 +25,7 @@ public static class DaprAiConversationBuilderExtensions
2425
/// <summary>
2526
/// Registers the necessary functionality for the Dapr AI Conversation functionality.
2627
/// </summary>
28+
[Experimental("DAPR_CONVERSATION", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/conversation/conversation-overview/")]
2729
public static IDaprAiConversationBuilder AddDaprConversationClient(
2830
this IServiceCollection services,
2931
Action<IServiceProvider, DaprConversationClientBuilder>? configure = null,

0 commit comments

Comments
 (0)