From 6a647d4878a9a12ddcb075a1f993cfee74a0a72d Mon Sep 17 00:00:00 2001 From: DaryaYuk Date: Sun, 1 Oct 2023 21:00:57 +0300 Subject: [PATCH 1/8] docs: update README.md Signed-off-by: DaryaYuk --- README.md | 294 +++++++++++++++++++++++-------------- release-please-config.json | 3 +- 2 files changed, 185 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index 7fa57c19..ccfb471d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ +

@@ -9,32 +10,50 @@

OpenFeature .NET SDK

-[![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) -[![spec version badge](https://img.shields.io/badge/Specification-v0.5.2-yellow)](https://github.com/open-feature/spec/tree/v0.5.2?rgh-link-date=2023-01-20T21%3A37%3A52Z) -[![codecov](https://codecov.io/gh/open-feature/dotnet-sdk/branch/main/graph/badge.svg?token=MONAVJBXUJ)](https://codecov.io/gh/open-feature/dotnet-sdk) -[![nuget](https://img.shields.io/nuget/vpre/OpenFeature)](https://www.nuget.org/packages/OpenFeature) -[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6250/badge)](https://bestpractices.coreinfrastructure.org/projects/6250) - -## 👋 Hey there! Thanks for checking out the OpenFeature .NET SDK - -### What is OpenFeature? + + +

+ + Specification + + + + + Release + + + +
+ + Slack + + + Codecov + + + Codecov + + + CII Best Practices + +

+ -[OpenFeature][openfeature-website] is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. +[OpenFeature](https://openfeature.dev) is an open standard that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. -### Why standardize feature flags? + -Standardizing feature flags unifies tools and vendors behind a common interface which avoids vendor lock-in at the code level. Additionally, it offers a framework for building extensions and integrations and allows providers to focus on their unique value proposition. +## 🚀 Quick start -## 🔍 Requirements: +### Requirements -- .NET 6+ -- .NET Core 6+ -- .NET Framework 4.6.2+ +- .NET 6+ +- .NET Core 6+ +- .NET Framework 4.6.2+ Note that the packages will aim to support all current .NET versions. Refer to the currently supported versions [.NET](https://dotnet.microsoft.com/download/dotnet) and [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) excluding .NET Framework 3.5 - -## 📦 Installation: +### Install Use the following to initialize your project: @@ -48,106 +67,93 @@ and install OpenFeature: dotnet add package OpenFeature ``` -## 🌟 Features: - -- support for various backend [providers](https://openfeature.dev/docs/reference/concepts/provider) -- easy integration and extension via [hooks](https://openfeature.dev/docs/reference/concepts/hooks) -- bool, string, numeric and object flag types -- [context-aware](https://openfeature.dev/docs/reference/concepts/evaluation-context) evaluation - -## 🚀 Usage: - -### Basics: +### Usage ```csharp -using OpenFeature.Model; - -// Sets the provider used by the client -// If no provider is set, then a default NoOpProvider will be used. -//OpenFeature.Api.Instance.SetProvider(new MyProvider()); - -// Gets a instance of the feature flag client -var client = OpenFeature.Api.Instance.GetClient(); -// Evaluation the `my-feature` feature flag -var isEnabled = await client.GetBooleanValue("my-feature", false); +public async Task Example() + { + // Register your feature flag provider + Api.Instance.SetProvider(new InMemoryProvider()); + + // Create a new client + FeatureClient client = Api.Instance.GetClient(); + + // Evaluate your feature flag + bool v2Enabled = await client.GetBooleanValue("v2_enabled", false); + + if ( v2Enabled ) + { + //Do some work + } + } ``` -For complete documentation, visit: https://openfeature.dev/docs/category/concepts +## 🌟 Features -### Context-aware evaluation: +| Status | Features | Description | +| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| ✅ | [Logging](#logging) | Integrate with popular logging packages. | +| ✅ | [Named clients](#named-clients) | Utilize multiple providers in a single application. | +| ❌ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| ❌ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | -Sometimes the value of a flag must take into account some dynamic criteria about the application or user, such as the user location, IP, email address, or the location of the server. -In OpenFeature, we refer to this as [`targeting`](https://openfeature.dev/specification/glossary#targeting). -If the flag system you're using supports targeting, you can provide the input data using the `EvaluationContext`. +Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ -```csharp -using OpenFeature.Model; +### Providers -var client = OpenFeature.Api.Instance.GetClient(); - -// Evaluating with a context. -var evaluationContext = EvaluationContext.Builder() - .Set("my-key", "my-value") - .Build(); - -// Evaluation the `my-conditional` feature flag -var isEnabled = await client.GetBooleanValue("my-conditional", false, evaluationContext); -``` +[Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. +Look [here](https://openfeature.dev/docs/reference/technologies/server/dotnet/) for a complete list of available providers. +If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. -### Providers: - -To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk-contrib) available under the OpenFeature organization. Finally, you’ll then need to write the provider itself. This can be accomplished by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. +Once you've added a provider as a dependency, it can be registered with OpenFeature like this: ```csharp -using OpenFeature; -using OpenFeature.Model; - -public class MyFeatureProvider : FeatureProvider -{ - public static string Name => "My Feature Provider"; - - public Metadata GetMetadata() - { - return new Metadata(Name); - } - - public Task> ResolveBooleanValue(string flagKey, bool defaultValue, - EvaluationContext context = null) - { - // code to resolve boolean details - } +Api.Instance.SetProvider(new MyProvider()); +``` - public Task> ResolveStringValue(string flagKey, string defaultValue, - EvaluationContext context = null) - { - // code to resolve string details - } +In some situations, it may be beneficial to register multiple providers in the same application. +This is possible using [named clients](#named-clients), which is covered in more details below. - public Task> ResolveIntegerValue(string flagKey, int defaultValue, - EvaluationContext context = null) - { - // code to resolve integer details - } +### Targeting - public Task> ResolveDoubleValue(string flagKey, double defaultValue, - EvaluationContext context = null) - { - // code to resolve integer details - } +Sometimes, the value of a flag must consider some dynamic criteria about the application or user such as the user's location, IP, email address, or the server's location. +In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting). +If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). - public Task> ResolveStructureValue(string flagKey, T defaultValue, - EvaluationContext context = null) - { - // code to resolve object details - } -} +```csharp +// set a value to the global context + EvaluationContextBuilder builder = EvaluationContext.Builder(); + builder.Set("region", "us-east-1"); + EvaluationContext apiCtx = builder.Build(); + Api.Instance.SetContext(apiCtx); + + // set a value to the client context + builder = EvaluationContext.Builder(); + builder.Set("region", "us-east-1"); + EvaluationContext clientCtx = builder.Build(); + var client = Api.Instance.GetClient(); + client.SetContext(clientCtx); + + + // set a value to the invocation context + builder = EvaluationContext.Builder(); + builder.Set("region", "us-east-1"); + EvaluationContext reqCtx = builder.Build(); + + bool flagValue = await client.GetBooleanValue("some-flag", false, reqCtx); ``` -See [here](https://openfeature.dev/docs/reference/technologies/server/dotnet) for a catalog of available providers. +### Hooks -### Hooks: +[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. +Look [here](https://openfeature.dev/docs/reference/technologies/server/dotnet/) for a complete list of available hooks. +If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. -Hooks are a mechanism that allow for the addition of arbitrary behavior at well-defined points of the flag evaluation life-cycle. Use cases include validation of the resolved flag value, modifying or adding data to the evaluation context, logging, telemetry, and tracking. +Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. ```csharp // add a hook globally, to run on all evaluations @@ -161,7 +167,77 @@ client.AddHooks(new ExampleClientHook()); var value = await client.GetBooleanValue("boolFlag", false, context, new FlagEvaluationOptions(new ExampleInvocationHook())); ``` -Example of implementing a hook +### Logging + +The .NET SDK uses Microsoft Extensions Logger. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. + +### Named clients + +Clients can be given a name. +A name is a logical identifier which can be used to associate clients with a particular provider. +If a name has no associated provider, the global provider is used. + +```csharp +// registering the default provider + Api.Instance.SetProvider(new LocalProvider()); + // registering a named provider + Api.Instance.SetProvider("clientForCache", new CachedProvider()); + + // a client backed by default provider + FeatureClient clientDefault = Api.Instance.GetClient(); + // a client backed by CachedProvider + FeatureClient clientNamed = Api.Instance.GetClient("clientForCache"); +``` + +## Extending + +### Develop a provider + +To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk) available under the OpenFeature organization. +You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. + +```csharp +public class MyProvider : FeatureProvider + { + public override Metadata GetMetadata() + { + return new Metadata("My Provider"); + } + + public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) + { + // resolve a boolean flag value + } + + public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null) + { + // resolve a double flag value + } + + public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null) + { + // resolve an int flag value + } + + public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null) + { + // resolve a string flag value + } + + public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null) + { + // resolve an object flag value + } + } +``` + +### Develop a hook + +To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/dotnet-sdk) available under the OpenFeature organization. +Implement your own hook by conforming to the `Hook interface`. +To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined. ```csharp public class MyHook : Hook @@ -191,20 +267,16 @@ public class MyHook : Hook } ``` -See [here](https://openfeature.dev/docs/reference/technologies/server/dotnet) for a catalog of available hooks. - -### Logging: - -The .NET SDK uses Microsoft Extensions Logger. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. +Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs! ## ⭐️ Support the project -- Give this repo a ⭐️! -- Follow us social media: - - Twitter: [@openfeature](https://twitter.com/openfeature) - - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) -- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) -- For more check out our [community page](https://openfeature.dev/community/) +- Give this repo a ⭐️! +- Follow us social media: + - Twitter: [@openfeature](https://twitter.com/openfeature) + - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) +- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) +- For more check out our [community page](https://openfeature.dev/community/) ## 🤝 Contributing diff --git a/release-please-config.json b/release-please-config.json index 3a34941e..1c1e673c 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -8,7 +8,8 @@ "bump-patch-for-minor-pre-major": true, "versioning": "default", "extra-files": [ - "build/Common.prod.props" + "build/Common.prod.props", + "README.md" ] } }, From a764d2461e18304fc7cd62c2e7e05cc6e20e1a4e Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Tue, 3 Oct 2023 14:14:48 -0400 Subject: [PATCH 2/8] chore: update header image Signed-off-by: Michael Beemer Signed-off-by: DaryaYuk --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ccfb471d..608d9649 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,8 @@

- - - OpenFeature Logo + + OpenFeature Logo

From 9fa62c71601cdd195d122038bcb959a64f603788 Mon Sep 17 00:00:00 2001 From: Darya <145902078+DaryaYuk@users.noreply.github.com> Date: Wed, 4 Oct 2023 21:45:46 +0300 Subject: [PATCH 3/8] Update README.md Co-authored-by: Michael Beemer Signed-off-by: Darya <145902078+DaryaYuk@users.noreply.github.com> Signed-off-by: DaryaYuk --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 608d9649..7444363d 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,8 @@ Codecov - CII Best Practices + CII Best Practices +

From 7a092be1b88b17239f8decc4c3166a9781ab3cba Mon Sep 17 00:00:00 2001 From: Darya <145902078+DaryaYuk@users.noreply.github.com> Date: Wed, 4 Oct 2023 21:46:55 +0300 Subject: [PATCH 4/8] Update README.md Co-authored-by: Michael Beemer Signed-off-by: Darya <145902078+DaryaYuk@users.noreply.github.com> Signed-off-by: DaryaYuk --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7444363d..a59015e8 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,8 @@ public async Task Example() ### Providers [Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. -Look [here](https://openfeature.dev/docs/reference/technologies/server/dotnet/) for a complete list of available providers. +Look [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET) for a complete list of available providers. + If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. Once you've added a provider as a dependency, it can be registered with OpenFeature like this: From e160058efe7fd86608935bb6adab55201330c9b8 Mon Sep 17 00:00:00 2001 From: Darya <145902078+DaryaYuk@users.noreply.github.com> Date: Wed, 4 Oct 2023 21:47:24 +0300 Subject: [PATCH 5/8] Update README.md Co-authored-by: Michael Beemer Signed-off-by: Darya <145902078+DaryaYuk@users.noreply.github.com> Signed-off-by: DaryaYuk --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a59015e8..a51b354f 100644 --- a/README.md +++ b/README.md @@ -180,14 +180,15 @@ If a name has no associated provider, the global provider is used. ```csharp // registering the default provider - Api.Instance.SetProvider(new LocalProvider()); - // registering a named provider - Api.Instance.SetProvider("clientForCache", new CachedProvider()); - - // a client backed by default provider - FeatureClient clientDefault = Api.Instance.GetClient(); - // a client backed by CachedProvider - FeatureClient clientNamed = Api.Instance.GetClient("clientForCache"); +Api.Instance.SetProvider(new LocalProvider()); +// registering a named provider +Api.Instance.SetProvider("clientForCache", new CachedProvider()); + +// a client backed by default provider + FeatureClient clientDefault = Api.Instance.GetClient(); +// a client backed by CachedProvider +FeatureClient clientNamed = Api.Instance.GetClient("clientForCache"); + ``` ## Extending From 554e902242573a11569419010a764e0390f4ae0a Mon Sep 17 00:00:00 2001 From: Darya <145902078+DaryaYuk@users.noreply.github.com> Date: Wed, 4 Oct 2023 21:47:42 +0300 Subject: [PATCH 6/8] Update README.md Co-authored-by: Michael Beemer Signed-off-by: Darya <145902078+DaryaYuk@users.noreply.github.com> Signed-off-by: DaryaYuk --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index a51b354f..8d7b5856 100644 --- a/README.md +++ b/README.md @@ -127,25 +127,25 @@ If the flag management system you're using supports targeting, you can provide t ```csharp // set a value to the global context - EvaluationContextBuilder builder = EvaluationContext.Builder(); - builder.Set("region", "us-east-1"); - EvaluationContext apiCtx = builder.Build(); - Api.Instance.SetContext(apiCtx); - - // set a value to the client context - builder = EvaluationContext.Builder(); - builder.Set("region", "us-east-1"); - EvaluationContext clientCtx = builder.Build(); - var client = Api.Instance.GetClient(); - client.SetContext(clientCtx); +EvaluationContextBuilder builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext apiCtx = builder.Build(); +Api.Instance.SetContext(apiCtx); + +// set a value to the client context +builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext clientCtx = builder.Build(); +var client = Api.Instance.GetClient(); +client.SetContext(clientCtx); +// set a value to the invocation context +builder = EvaluationContext.Builder(); +builder.Set("region", "us-east-1"); +EvaluationContext reqCtx = builder.Build(); - // set a value to the invocation context - builder = EvaluationContext.Builder(); - builder.Set("region", "us-east-1"); - EvaluationContext reqCtx = builder.Build(); +bool flagValue = await client.GetBooleanValue("some-flag", false, reqCtx); - bool flagValue = await client.GetBooleanValue("some-flag", false, reqCtx); ``` ### Hooks From 592e516840b45e9797eb4c1c80118417ed0ad7a0 Mon Sep 17 00:00:00 2001 From: DaryaYuk Date: Wed, 4 Oct 2023 22:14:53 +0300 Subject: [PATCH 7/8] docs: update README.md Signed-off-by: DaryaYuk --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8d7b5856..c0fe5f15 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@

- - Specification + + Specification From f8e1f1d674329f2b43bc54ea096ce019cb8c9cce Mon Sep 17 00:00:00 2001 From: DaryaYuk Date: Thu, 5 Oct 2023 21:56:07 +0300 Subject: [PATCH 8/8] docs: fix grammatical errors in README.md Signed-off-by: DaryaYuk --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c0fe5f15..3693d785 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ public async Task Example() ### Providers [Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. -Look [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET) for a complete list of available providers. +Here is [a complete list of available providers](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET). If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. @@ -117,7 +117,7 @@ Api.Instance.SetProvider(new MyProvider()); ``` In some situations, it may be beneficial to register multiple providers in the same application. -This is possible using [named clients](#named-clients), which is covered in more details below. +This is possible using [named clients](#named-clients), which is covered in more detail below. ### Targeting @@ -151,7 +151,7 @@ bool flagValue = await client.GetBooleanValue("some-flag", false, reqCtx); ### Hooks [Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. -Look [here](https://openfeature.dev/docs/reference/technologies/server/dotnet/) for a complete list of available hooks. +Here is [a complete list of available hooks](https://openfeature.dev/docs/reference/technologies/server/dotnet/). If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. @@ -170,12 +170,12 @@ var value = await client.GetBooleanValue("boolFlag", false, context, new FlagEva ### Logging -The .NET SDK uses Microsoft Extensions Logger. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for complete documentation. +The .NET SDK uses Microsoft.Extensions.Logging. See the [manual](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line) for a complete documentation. ### Named clients Clients can be given a name. -A name is a logical identifier which can be used to associate clients with a particular provider. +A name is a logical identifier that can be used to associate clients with a particular provider. If a name has no associated provider, the global provider is used. ```csharp @@ -274,17 +274,17 @@ Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/ ## ⭐️ Support the project - Give this repo a ⭐️! -- Follow us social media: +- Follow us on social media: - Twitter: [@openfeature](https://twitter.com/openfeature) - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/) - Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1) -- For more check out our [community page](https://openfeature.dev/community/) +- For more information, check out our [community page](https://openfeature.dev/community/) ## 🤝 Contributing Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. -### Thanks to everyone that has already contributed +### Thanks to everyone who has already contributed