├── .config └── dotnet-tools.json ├── .dockerignore ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── stale.yml └── workflows │ ├── add-to-project.yml │ ├── prs.yml │ └── release.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── Unleash.sln ├── docker-compose.yml ├── resources ├── dashboard.png └── logo.png ├── samples ├── BootstrapHost │ ├── BootstrapHost.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot │ │ └── bootstrap.json ├── DotnetCore │ ├── DotnetCore.csproj │ ├── DotnetCore.sln │ └── Program.cs ├── WebApp │ ├── App_Start │ │ ├── BundleConfig.cs │ │ ├── FilterConfig.cs │ │ ├── RouteConfig.cs │ │ └── WebApiConfig.cs │ ├── ApplicationInsights.config │ ├── Areas │ │ └── HelpPage │ │ │ ├── ApiDescriptionExtensions.cs │ │ │ ├── App_Start │ │ │ └── HelpPageConfig.cs │ │ │ ├── Controllers │ │ │ └── HelpController.cs │ │ │ ├── HelpPage.css │ │ │ ├── HelpPageAreaRegistration.cs │ │ │ ├── HelpPageConfigurationExtensions.cs │ │ │ ├── ModelDescriptions │ │ │ ├── CollectionModelDescription.cs │ │ │ ├── ComplexTypeModelDescription.cs │ │ │ ├── DictionaryModelDescription.cs │ │ │ ├── EnumTypeModelDescription.cs │ │ │ ├── EnumValueDescription.cs │ │ │ ├── IModelDocumentationProvider.cs │ │ │ ├── KeyValuePairModelDescription.cs │ │ │ ├── ModelDescription.cs │ │ │ ├── ModelDescriptionGenerator.cs │ │ │ ├── ModelNameAttribute.cs │ │ │ ├── ModelNameHelper.cs │ │ │ ├── ParameterAnnotation.cs │ │ │ ├── ParameterDescription.cs │ │ │ └── SimpleTypeModelDescription.cs │ │ │ ├── Models │ │ │ └── HelpPageApiModel.cs │ │ │ ├── SampleGeneration │ │ │ ├── HelpPageSampleGenerator.cs │ │ │ ├── HelpPageSampleKey.cs │ │ │ ├── ImageSample.cs │ │ │ ├── InvalidSample.cs │ │ │ ├── ObjectGenerator.cs │ │ │ ├── SampleDirection.cs │ │ │ └── TextSample.cs │ │ │ ├── Views │ │ │ ├── Help │ │ │ │ ├── Api.cshtml │ │ │ │ ├── DisplayTemplates │ │ │ │ │ ├── ApiGroup.cshtml │ │ │ │ │ ├── CollectionModelDescription.cshtml │ │ │ │ │ ├── ComplexTypeModelDescription.cshtml │ │ │ │ │ ├── DictionaryModelDescription.cshtml │ │ │ │ │ ├── EnumTypeModelDescription.cshtml │ │ │ │ │ ├── HelpPageApiModel.cshtml │ │ │ │ │ ├── ImageSample.cshtml │ │ │ │ │ ├── InvalidSample.cshtml │ │ │ │ │ ├── KeyValuePairModelDescription.cshtml │ │ │ │ │ ├── ModelDescriptionLink.cshtml │ │ │ │ │ ├── Parameters.cshtml │ │ │ │ │ ├── Samples.cshtml │ │ │ │ │ ├── SimpleTypeModelDescription.cshtml │ │ │ │ │ └── TextSample.cshtml │ │ │ │ ├── Index.cshtml │ │ │ │ └── ResourceModel.cshtml │ │ │ ├── Shared │ │ │ │ └── _Layout.cshtml │ │ │ ├── Web.config │ │ │ └── _ViewStart.cshtml │ │ │ └── XmlDocumentationProvider.cs │ ├── Content │ │ ├── Site.css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap.css │ │ └── bootstrap.min.css │ ├── Controllers │ │ ├── HomeController.cs │ │ └── ValuesController.cs │ ├── Global.asax │ ├── Global.asax.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Scripts │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ ├── jquery-1.10.2.intellisense.js │ │ ├── jquery-1.10.2.js │ │ ├── jquery-1.10.2.min.js │ │ ├── jquery-1.10.2.min.map │ │ ├── modernizr-2.6.2.js │ │ ├── respond.js │ │ └── respond.min.js │ ├── Views │ │ ├── Home │ │ │ └── Index.cshtml │ │ ├── Shared │ │ │ ├── Error.cshtml │ │ │ └── _Layout.cshtml │ │ ├── Web.config │ │ └── _ViewStart.cshtml │ ├── Web.Debug.config │ ├── Web.Release.config │ ├── Web.config │ ├── WebApp.csproj │ ├── favicon.ico │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ └── packages.config ├── WebAppCore │ ├── Pages │ │ ├── About.cshtml │ │ ├── About.cshtml.cs │ │ ├── Contact.cshtml │ │ ├── Contact.cshtml.cs │ │ ├── Error.cshtml │ │ ├── Error.cshtml.cs │ │ ├── Index.cshtml │ │ ├── Index.cshtml.cs │ │ ├── _Layout.cshtml │ │ ├── _ValidationScriptsPartial.cshtml │ │ ├── _ViewImports.cshtml │ │ └── _ViewStart.cshtml │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ ├── WebAppCore.csproj │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── bundleconfig.json │ └── wwwroot │ │ ├── css │ │ ├── site.css │ │ └── site.min.css │ │ ├── favicon.ico │ │ ├── images │ │ ├── banner1.svg │ │ ├── banner2.svg │ │ ├── banner3.svg │ │ └── banner4.svg │ │ ├── js │ │ ├── site.js │ │ └── site.min.js │ │ └── lib │ │ ├── bootstrap │ │ ├── .bower.json │ │ ├── LICENSE │ │ └── dist │ │ │ ├── css │ │ │ ├── bootstrap-theme.css │ │ │ ├── bootstrap-theme.css.map │ │ │ ├── bootstrap-theme.min.css.map │ │ │ ├── bootstrap.css │ │ │ ├── bootstrap.css.map │ │ │ └── bootstrap.min.css.map │ │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ │ └── js │ │ │ ├── bootstrap.js │ │ │ └── npm.js │ │ ├── jquery-validation-unobtrusive │ │ ├── .bower.json │ │ ├── jquery.validate.unobtrusive.js │ │ └── jquery.validate.unobtrusive.min.js │ │ ├── jquery-validation │ │ ├── .bower.json │ │ ├── LICENSE.md │ │ └── dist │ │ │ ├── additional-methods.js │ │ │ └── jquery.validate.js │ │ └── jquery │ │ ├── .bower.json │ │ ├── LICENSE.txt │ │ └── dist │ │ ├── jquery.js │ │ └── jquery.min.map ├── WebApplication │ ├── Controllers │ │ └── UnleashController.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ ├── WebApplication.csproj │ ├── appsettings.Development.json │ └── appsettings.json └── WinFormsApp │ ├── App.config │ ├── Program.cs │ ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings │ ├── UnleashForm.Designer.cs │ ├── UnleashForm.cs │ ├── UnleashForm.resx │ ├── WinFormsApp.csproj │ └── packages.config ├── src └── Unleash │ ├── ActivationStrategy.cs │ ├── ClientFactory │ ├── IUnleashClientFactory.cs │ └── UnleashClientFactory.cs │ ├── Communication │ ├── FetchTogglesResult.cs │ ├── IUnleashApiClient.cs │ ├── UnleashApiClient.cs │ └── UnleashApiClientRequestHeaders.cs │ ├── DefaultCustomHttpHeaderProvider.cs │ ├── DefaultHttpClientFactory.cs │ ├── DefaultUnleash.cs │ ├── DefaultUnleashContextProvider.cs │ ├── Events │ ├── ErrorEvent.cs │ └── ErrorType.cs │ ├── FakeUnleash.cs │ ├── IHttpClientFactory.cs │ ├── IUnleash.cs │ ├── IUnleashContextProvider.cs │ ├── IUnleashCustomHttpHeaderProvider.cs │ ├── Internal │ ├── CachedFilesLoader.cs │ ├── EventCallbackConfig.cs │ ├── FileSystem.cs │ ├── IFileSystem.cs │ ├── IToggleBootstrapProvider.cs │ ├── ImpressionEvent.cs │ ├── ToggleDefinition.cs │ ├── TogglesUpdatedEvent.cs │ ├── UnleashExtensions.cs │ ├── UnleashServices.cs │ ├── UnleashSettingsValidator.cs │ └── Variant.cs │ ├── LibLog.cs │ ├── Metrics │ ├── ClientMetrics.cs │ ├── ClientRegistration.cs │ └── MetricsMetadata.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── Scheduling │ ├── ClientMetricsBackgroundTask.cs │ ├── ClientRegistrationBackgroundTask.cs │ ├── FetchFeatureTogglesTask.cs │ ├── IUnleashScheduledTask.cs │ ├── IUnleashScheduledTaskManager.cs │ └── SystemTimerScheduledTaskManager.cs │ ├── Strategies │ └── IStrategy.cs │ ├── Streaming │ └── StreamingFeatureFetcher.cs │ ├── Unleash.csproj │ ├── UnleashContext.cs │ ├── UnleashException.cs │ ├── UnleashSettings.cs │ └── Utilities │ ├── FetchingToggleBootstrapUrlFailedException.cs │ ├── ToggleBootstrapFileProvider.cs │ ├── ToggleBootstrapUrlProvider.cs │ └── WarnOnce.cs ├── tests ├── Unleash.Tests.Benchmarks │ ├── App.config │ ├── Program.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Unleash.Tests.Benchmarks.csproj │ └── packages.config └── Unleash.Tests │ ├── App.config │ ├── App_Data │ ├── dependent-features-missing-enabled.json │ ├── empty-v1.json │ ├── empty.json │ ├── etag-12345.txt │ ├── features-v0.json │ ├── features-v1-empty.json │ ├── features-v1.json │ ├── impressiondata-v2.json │ ├── unleash-repo-v0.json │ ├── unleash-repo-v1.json │ └── unleash-repo-without-feature-field.json │ ├── BaseTest.cs │ ├── ClientFactory │ └── SyncStartupUnitTest.cs │ ├── Communication │ ├── BaseBackoffTest.cs │ ├── CustomHeadersUnitTest.cs │ ├── MockHttpClient.cs │ ├── RequestMessageHeadersTest.cs │ ├── UnleasRequestBodyUnitTest.cs │ ├── UnleashApiClient_Features_Backoff_Tests.cs │ ├── UnleashApiClient_Features_Tests.cs │ ├── UnleashApiClient_Metrics_Backoff_Tests.cs │ ├── UnleashApiClient_Project_Tests.cs │ ├── UnleashApiClient_RegisterClient_Tests.cs │ ├── UnleashApiClient_SendMetrics_Tests.cs │ └── UnleashHttpClientFactory_RegisterHttpClient_Tests.cs │ ├── DefaultUnleashTests.cs │ ├── ExampleTests.cs │ ├── FakeUnleashTests.cs │ ├── Integration │ ├── ClientSpecificationTests.cs │ ├── Data │ │ ├── .gitignore │ │ └── README │ ├── TestCase.cs │ ├── TestCaseVariant.cs │ ├── TestDefinition.cs │ └── UnleashContextDefinition.cs │ ├── Internal │ ├── CachedFilesLoaderTestBase.cs │ ├── CachedFilesLoader_Backup_And_Etag_Tests.cs │ ├── CachedFilesLoader_Bootstrap_Tests.cs │ ├── ErrorEvents_Tests.cs │ ├── ImpressionData_Tests.cs │ └── TogglesUpdatedEvent_Tests.cs │ ├── Metrics │ └── MetricsMetadataTests.cs │ ├── Mock │ ├── ConfigurableMessageHandlerMock.cs │ ├── HttpClientFactoryMock.cs │ ├── MockApiClient.cs │ ├── MockFileSystem.cs │ └── MockHttpMessageHandler.cs │ ├── MockedUnleashSettings.cs │ ├── NLog.config │ ├── NLog.xsd │ ├── Properties │ └── AssemblyInfo.cs │ ├── Streaming │ └── StreamingFeatureFetcherTests.cs │ ├── Unleash.Tests.csproj │ ├── UnleashContextTests.cs │ ├── UnleashSettingsTests.cs │ ├── Utilities │ ├── ToggleBootstrapFileProvider_Tests.cs │ └── ToggleBootstrapUrlProvider_Tests.cs │ └── packages.config ├── tools └── packages.config └── v5_MIGRATION_GUIDE.md /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "cake.tool": { 6 | "version": "1.3.0", 7 | "commands": [ 8 | "dotnet-cake" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.dockerignore 2 | **/.env 3 | **/.git 4 | **/.gitignore 5 | **/.vs 6 | **/.vscode 7 | **/*.*proj.user 8 | **/azds.yaml 9 | **/charts 10 | **/bin 11 | **/obj 12 | **/Dockerfile 13 | **/Dockerfile.develop 14 | **/docker-compose.yml 15 | **/docker-compose.*.yml 16 | **/*.dbmdl 17 | **/*.jfm 18 | **/secrets.dev.yaml 19 | **/values.dev.yaml 20 | **/.toolstarget -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | # How Has This Been Tested? 17 | 18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 19 | 20 | - [ ] Test A 21 | - [ ] Test B 22 | 23 | **Test Configuration**: 24 | * Firmware version: 25 | * Hardware: 26 | * Toolchain: 27 | * SDK: 28 | 29 | # Checklist: 30 | 31 | - [ ] My code follows the style guidelines of this project 32 | - [ ] I have performed a self-review of my own code 33 | - [ ] I have commented my code, particularly in hard-to-understand areas 34 | - [ ] I have made corresponding changes to the documentation 35 | - [ ] My changes generate no new warnings 36 | - [ ] I have added tests that prove my fix is effective or that my feature works 37 | - [ ] New and existing unit tests pass locally with my changes 38 | - [ ] Any dependent changes have been merged and published in downstream modules -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | -------------------------------------------------------------------------------- /.github/workflows/add-to-project.yml: -------------------------------------------------------------------------------- 1 | name: Add new item to project board 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | pull_request_target: 8 | types: 9 | - opened 10 | 11 | jobs: 12 | add-to-project: 13 | uses: unleash/.github/.github/workflows/add-item-to-project.yml@main 14 | secrets: inherit 15 | -------------------------------------------------------------------------------- /.github/workflows/prs.yml: -------------------------------------------------------------------------------- 1 | name: Pull request 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | include: 14 | - os: ubuntu-latest 15 | - os: windows-latest 16 | - os: macos-13 17 | - os: macos-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - uses: actions/setup-dotnet@v4 23 | with: 24 | dotnet-version: '6.0.x' 25 | 26 | - name: Verify formatting 27 | run: dotnet format --verify-no-changes 28 | env: 29 | PATH: ${{ github.env.PATH }}:/home/runner/.dotnet/tools 30 | 31 | - name: Build test project 32 | run: dotnet build tests/Unleash.Tests/Unleash.Tests.csproj --no-restore --configuration Release 33 | 34 | - name: Run tests 35 | run: dotnet test tests/Unleash.Tests/Unleash.Tests.csproj --no-build --configuration Release --verbosity minimal 36 | 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This workflow is run when releasing and is responsible for packing and pushing Nuget updates 2 | 3 | name: Release 4 | 5 | on: 6 | release: 7 | types: [ published ] 8 | 9 | jobs: 10 | build: 11 | runs-on: windows-2022 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | # Set up MSBuild in PATH for command 17 | - name: Setup MSBuild in PATH 18 | uses: microsoft/setup-msbuild@v1 19 | 20 | - uses: actions/setup-dotnet@v1 21 | with: 22 | dotnet-version: '6.0.x' 23 | 24 | # Get NuGet setup for restoring, packaging and pushing 25 | - name: Setup NuGet 26 | uses: NuGet/setup-nuget@v1.0.5 27 | 28 | - name: Restore packages with NuGet 29 | run: nuget restore Unleash.sln 30 | 31 | # Get the versioning build variables in place 32 | - name: Set versioning build variables 33 | env: 34 | NUGET_VERSION: ${{ github.event.release.tag_name }} 35 | run: | 36 | echo "nuget=${NUGET_VERSION}" >> $GITHUB_ENV 37 | echo "file=${NUGET_VERSION%-beta*}" >> $GITHUB_ENV 38 | echo "assembly=${NUGET_VERSION%-beta*}" >> $GITHUB_ENV 39 | shell: bash 40 | 41 | - name: Build the solution 42 | run: msbuild Unleash.sln /p:Configuration=Release /p:Version=${{ env.nuget }} /p:FileVersion=${{ env.file }} /p:AssemblyVersion=${{ env.assembly }} 43 | 44 | - name: Publish package 45 | run: nuget push **\Unleash.Client.*.nupkg -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{ secrets.NUGET_API_KEY }} 46 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceRoot}/src/samples/WebAppCore/bin/Debug/netcoreapp2.0/WebAppCore.dll", 14 | "args": [], 15 | "cwd": "${workspaceRoot}/src/samples/WebAppCore", 16 | "stopAtEntry": false, 17 | "internalConsoleOptions": "openOnSessionStart", 18 | "launchBrowser": { 19 | "enabled": true, 20 | "args": "${auto-detect-url}", 21 | "windows": { 22 | "command": "cmd.exe", 23 | "args": "/C start ${auto-detect-url}" 24 | }, 25 | "osx": { 26 | "command": "open" 27 | }, 28 | "linux": { 29 | "command": "xdg-open" 30 | } 31 | }, 32 | "env": { 33 | "ASPNETCORE_ENVIRONMENT": "Development" 34 | }, 35 | "sourceFileMap": { 36 | "/Views": "${workspaceRoot}/Views" 37 | } 38 | }, 39 | { 40 | "name": ".NET Core Attach", 41 | "type": "coreclr", 42 | "request": "attach", 43 | "processId": "${command:pickProcess}" 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "command": "dotnet", 4 | "args": [], 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "type": "shell", 9 | "command": "dotnet", 10 | "args": [ 11 | "build", 12 | "${workspaceRoot}/src/samples/WebAppCore/WebAppCore.csproj" 13 | ], 14 | "problemMatcher": "$msCompile", 15 | "group": { 16 | "_id": "build", 17 | "isDefault": false 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | 6 | 7 | ## [1.6.1] - 2020-10-20 8 | - fix timing bug in SystemTimerScheduledTaskManager 9 | 10 | ## [1.6.0] - 2020-10-04 11 | - Add support for custom strategies in IUnleashClientFactory create methods 12 | 13 | ## [1.5.1] - 2020-08-21 14 | ### Changed 15 | - Ignore `TaskCanceledException` if cancellation is requested, and downgrade from error to warning if not. 16 | 17 | ## [1.5.0] - 2020-07-26 18 | ### Added 19 | - Introduce synchronous `IUnleashClientFactory.CreateClient` 20 | ### Changed 21 | - Rename `IUnleashClientFactory.Generate` to `CreateClientAsync` 22 | 23 | ## [1.4.1] - 2020-06-23 24 | ### Fixed 25 | - Support API URI with and without trailing slash (#58) 26 | 27 | ## [1.4.0] - 2020-02-26 28 | ### Added 29 | - Support synchronous load of toggles during startup (pull request #53) 30 | - Overloads for `IUnleash.IsEnabled` 31 | - `bool IsEnabled(string toggleName, UnleashContext context)` (pull request #56) 32 | - `bool IsEnabled(string toggleName, UnleashContext context, bool defaultSetting)` (pull request #55) 33 | ### Changed 34 | - Assembly and package name changed from `Unleash.Client.Core` to `Unleash.Client` 35 | 36 | ## [1.3.6] - 2020-02-09 37 | ### Added 38 | - Static context fields support (pull request #47) 39 | - Contraints support (#48) 40 | - CustomHttpHeaderProvider (pull request #52) 41 | ### Changed 42 | - Don't prefix API endpoint with `api/` (pull request #43) 43 | - Use murmur hashing (pull request #44) 44 | - Align Variants implementation with Unleash server (#45) 45 | ### Fixed 46 | - `HttpClient` allocation issue (pull request #41) 47 | 48 | ## [1.3.5] - 2019-10-02 49 | ### Added 50 | - Initial Variants support 51 | 52 | ## [1.3.4] - 2019-09-01 53 | ### Changed 54 | - Log exception on error (pull request #6) 55 | - Use Microsoft.CSharp from NuGet package when buliding .NET Standard. (pull request #7) 56 | 57 | ## [1.3.3] - 2018-30-07 58 | ### Changed 59 | - Typo fix (pull request #5) 60 | 61 | ## [1.3.2] - 2018-21-06 62 | ### Changed 63 | - Use relative url (pull request #3) 64 | 65 | ## [1.3.1] - 2018-04-13 66 | ### Changed 67 | - In ApplicationHostnameStrategy, hostname is set as the in other library implementations (hostname environment variable, and if not set; Dns.GetHostName()). 68 | 69 | ## [1.3.0] - 2017-12-04 70 | ### Changed 71 | - Better error handling during api server downtime etc. 72 | - Improved object synchronization (multiple readers/single writer) for MetricsBucket / ToggleCollection 73 | - Internal refactoring 74 | 75 | ## [1.2.2] - 2017-11-30 76 | ### Changed 77 | - Changed IJsonSerializer interface. 78 | 79 | ## [1.2.1] - 2017-11-30 80 | ### Added 81 | - Major internal refactorings 82 | - More extension points for users of the library 83 | 84 | ### Changed 85 | - UnleashConfig became UnleashSetting without fluent interface 86 | 87 | ## [1.1.2] - 2017-11-24 88 | ### Added 89 | - First draft of the library 90 | 91 | 92 | 102 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-buster-slim AS base 2 | WORKDIR /app 3 | EXPOSE 80 4 | EXPOSE 443 5 | 6 | FROM mcr.microsoft.com/dotnet/core/sdk:3.0-buster AS build 7 | WORKDIR / 8 | COPY ["samples/WebApplication/WebApplication.csproj", "samples/WebApplication/"] 9 | COPY ["src/Unleash/Unleash.csproj", "src/Unleash/"] 10 | RUN dotnet restore "samples/WebApplication/WebApplication.csproj" 11 | COPY . . 12 | WORKDIR "/samples/WebApplication" 13 | RUN dotnet build "WebApplication.csproj" -c Release -o /app/build 14 | 15 | FROM build AS publish 16 | RUN dotnet publish "WebApplication.csproj" -c Release -o /app/publish 17 | 18 | FROM base AS final 19 | WORKDIR /app 20 | COPY --from=publish /app/publish . 21 | ENTRYPOINT ["dotnet", "WebApplication.dll"] -------------------------------------------------------------------------------- /Unleash.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 25.0.1705.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unleash", "src\Unleash\Unleash.csproj", "{4CF2127B-8FC8-46FC-8614-3918148463A5}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unleash.Tests", "tests\Unleash.Tests\Unleash.Tests.csproj", "{FF268018-34C4-45EB-937E-2E0AB25EEEDA}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".docs", ".docs", "{F098FC57-35D3-4289-8040-B11161E2AB0D}" 11 | ProjectSection(SolutionItems) = preProject 12 | .gitignore = .gitignore 13 | appveyor.yml = appveyor.yml 14 | build.cake = build.cake 15 | CHANGELOG.md = CHANGELOG.md 16 | README.md = README.md 17 | EndProjectSection 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{29C4317B-9580-4E56-ACE0-06D3E74269D1}" 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{A703F9CB-157F-4166-B81D-27779B586DCB}" 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Release|Any CPU = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {4CF2127B-8FC8-46FC-8614-3918148463A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {4CF2127B-8FC8-46FC-8614-3918148463A5}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {4CF2127B-8FC8-46FC-8614-3918148463A5}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {4CF2127B-8FC8-46FC-8614-3918148463A5}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {FF268018-34C4-45EB-937E-2E0AB25EEEDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {FF268018-34C4-45EB-937E-2E0AB25EEEDA}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {FF268018-34C4-45EB-937E-2E0AB25EEEDA}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {FF268018-34C4-45EB-937E-2E0AB25EEEDA}.Release|Any CPU.Build.0 = Release|Any CPU 37 | EndGlobalSection 38 | GlobalSection(SolutionProperties) = preSolution 39 | HideSolutionNode = FALSE 40 | EndGlobalSection 41 | GlobalSection(NestedProjects) = preSolution 42 | {4CF2127B-8FC8-46FC-8614-3918148463A5} = {29C4317B-9580-4E56-ACE0-06D3E74269D1} 43 | {FF268018-34C4-45EB-937E-2E0AB25EEEDA} = {A703F9CB-157F-4166-B81D-27779B586DCB} 44 | EndGlobalSection 45 | GlobalSection(ExtensibilityGlobals) = postSolution 46 | SolutionGuid = {C8777E71-3209-4072-B7D9-466A4F5C7A75} 47 | EndGlobalSection 48 | GlobalSection(Performance) = preSolution 49 | HasPerformanceSessions = true 50 | EndGlobalSection 51 | EndGlobal 52 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | api: 4 | build: . 5 | ports: 6 | - "80:80" 7 | - "443:443" 8 | depends_on: 9 | - web 10 | web: 11 | image: unleashorg/unleash-server 12 | ports: 13 | - "4242:4242" 14 | environment: 15 | DATABASE_URL: postgres://postgres:unleash@db/postgres 16 | depends_on: 17 | - db 18 | command: > 19 | sh -c " 20 | while ! nc -z db 5432; do 21 | echo 'Postgres is unavailable.' 22 | sleep 1 23 | done 24 | npm run start" 25 | db: 26 | expose: 27 | - "5432" 28 | image: postgres:10-alpine -------------------------------------------------------------------------------- /resources/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-dotnet-sdk/8094dfe89d20e82c51f9ffb8b2570d0c931fe4b7/resources/dashboard.png -------------------------------------------------------------------------------- /resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-dotnet-sdk/8094dfe89d20e82c51f9ffb8b2570d0c931fe4b7/resources/logo.png -------------------------------------------------------------------------------- /samples/BootstrapHost/BootstrapHost.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /samples/BootstrapHost/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace BootstrapHost 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/BootstrapHost/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:41028", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "BootstrapHost": { 19 | "commandName": "Project", 20 | "dotnetRunMessages": "true", 21 | "launchBrowser": true, 22 | "applicationUrl": "http://localhost:5000", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /samples/BootstrapHost/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | 11 | namespace BootstrapHost 12 | { 13 | public class Startup 14 | { 15 | public Startup(IConfiguration configuration) 16 | { 17 | Configuration = configuration; 18 | } 19 | 20 | public IConfiguration Configuration { get; } 21 | 22 | // This method gets called by the runtime. Use this method to add services to the container. 23 | public void ConfigureServices(IServiceCollection services) 24 | { 25 | } 26 | 27 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 28 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 29 | { 30 | if (env.IsDevelopment()) 31 | { 32 | app.UseDeveloperExceptionPage(); 33 | } 34 | else 35 | { 36 | app.UseExceptionHandler("/Error"); 37 | } 38 | 39 | app.UseStaticFiles(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /samples/BootstrapHost/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft": "Warning", 7 | "Microsoft.Hosting.Lifetime": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/BootstrapHost/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /samples/BootstrapHost/wwwroot/bootstrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "features": [ 4 | { 5 | "name": "Demo123", 6 | "enabled": true, 7 | "strategies": [ 8 | { 9 | "name": "default" 10 | } 11 | ] 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /samples/DotnetCore/DotnetCore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | {4cf2127b-8fc8-46fc-8614-3918148463a5} 13 | Unleash 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/DotnetCore/DotnetCore.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.002.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotnetCore", "DotnetCore.csproj", "{CE83946A-DD8A-4D2E-BC85-0B1F6E9A72CE}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {CE83946A-DD8A-4D2E-BC85-0B1F6E9A72CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {CE83946A-DD8A-4D2E-BC85-0B1F6E9A72CE}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {CE83946A-DD8A-4D2E-BC85-0B1F6E9A72CE}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {CE83946A-DD8A-4D2E-BC85-0B1F6E9A72CE}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {8085F125-1F8A-4991-BEC4-B7ADD45BA1C0} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /samples/DotnetCore/Program.cs: -------------------------------------------------------------------------------- 1 | using Unleash; 2 | using Unleash.ClientFactory; 3 | using Unleash.Strategies; 4 | 5 | var settings = new UnleashSettings() 6 | { 7 | AppName = "dotnet-test", 8 | UnleashApi = new Uri("http://localhost:4242/api"), //setup for running against a local unleash instance, feel free to change this 9 | CustomHttpHeaders = new Dictionary() 10 | { 11 | {"Authorization","add a valid client token here" } 12 | }, 13 | SendMetricsInterval = TimeSpan.FromSeconds(1) 14 | }; 15 | 16 | const string TOGGLE_NAME = "test"; 17 | 18 | Console.WriteLine("Starting Unleash SDK"); 19 | 20 | var unleashFactory = new UnleashClientFactory(); 21 | IUnleash unleash = await unleashFactory.CreateClientAsync(settings, synchronousInitialization: true, new MyCustomStrategy()); 22 | 23 | 24 | while (true) 25 | { 26 | var enabled = unleash.IsEnabled(TOGGLE_NAME); 27 | var variant = unleash.GetVariant(TOGGLE_NAME); 28 | 29 | Console.WriteLine($"Toggle enabled: {enabled}, variant: {System.Text.Json.JsonSerializer.Serialize(variant)}"); 30 | await Task.Delay(1000); 31 | } 32 | 33 | // If you want to test this, you'll need to setup a custom strategy in your 34 | // Unleash UI and add it to the 'test' toggle. 35 | class MyCustomStrategy : IStrategy 36 | { 37 | public string Name => "my-custom-strategy"; 38 | 39 | public bool IsEnabled(Dictionary parameters, UnleashContext context) 40 | { 41 | return true; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /samples/WebApp/App_Start/BundleConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Web; 2 | using System.Web.Optimization; 3 | 4 | namespace WebApp 5 | { 6 | public class BundleConfig 7 | { 8 | // For more information on bundling, visit https://go.microsoft.com/fwlink/?LinkId=301862 9 | public static void RegisterBundles(BundleCollection bundles) 10 | { 11 | bundles.Add(new ScriptBundle("~/bundles/jquery").Include( 12 | "~/Scripts/jquery-{version}.js")); 13 | 14 | // Use the development version of Modernizr to develop with and learn from. Then, when you're 15 | // ready for production, use the build tool at https://modernizr.com to pick only the tests you need. 16 | bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( 17 | "~/Scripts/modernizr-*")); 18 | 19 | bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include( 20 | "~/Scripts/bootstrap.js", 21 | "~/Scripts/respond.js")); 22 | 23 | bundles.Add(new StyleBundle("~/Content/css").Include( 24 | "~/Content/bootstrap.css", 25 | "~/Content/site.css")); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /samples/WebApp/App_Start/FilterConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Web; 2 | using System.Web.Mvc; 3 | 4 | namespace WebApp 5 | { 6 | public class FilterConfig 7 | { 8 | public static void RegisterGlobalFilters(GlobalFilterCollection filters) 9 | { 10 | filters.Add(new HandleErrorAttribute()); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /samples/WebApp/App_Start/RouteConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using System.Web.Mvc; 6 | using System.Web.Routing; 7 | 8 | namespace WebApp 9 | { 10 | public class RouteConfig 11 | { 12 | public static void RegisterRoutes(RouteCollection routes) 13 | { 14 | routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 15 | 16 | routes.MapRoute( 17 | name: "Default", 18 | url: "{controller}/{action}/{id}", 19 | defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } 20 | ); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/WebApp/App_Start/WebApiConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web.Http; 5 | 6 | namespace WebApp 7 | { 8 | public static class WebApiConfig 9 | { 10 | public static void Register(HttpConfiguration config) 11 | { 12 | // Web API configuration and services 13 | 14 | // Web API routes 15 | config.MapHttpAttributeRoutes(); 16 | 17 | config.Routes.MapHttpRoute( 18 | name: "DefaultApi", 19 | routeTemplate: "api/{controller}/{id}", 20 | defaults: new { id = RouteParameter.Optional } 21 | ); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/ApiDescriptionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Web; 4 | using System.Web.Http.Description; 5 | 6 | namespace WebApp.Areas.HelpPage 7 | { 8 | public static class ApiDescriptionExtensions 9 | { 10 | /// 11 | /// Generates an URI-friendly ID for the . E.g. "Get-Values-id_name" instead of "GetValues/{id}?name={name}" 12 | /// 13 | /// The . 14 | /// The ID as a string. 15 | public static string GetFriendlyId(this ApiDescription description) 16 | { 17 | string path = description.RelativePath; 18 | string[] urlParts = path.Split('?'); 19 | string localPath = urlParts[0]; 20 | string queryKeyString = null; 21 | if (urlParts.Length > 1) 22 | { 23 | string query = urlParts[1]; 24 | string[] queryKeys = HttpUtility.ParseQueryString(query).AllKeys; 25 | queryKeyString = String.Join("_", queryKeys); 26 | } 27 | 28 | StringBuilder friendlyPath = new StringBuilder(); 29 | friendlyPath.AppendFormat("{0}-{1}", 30 | description.HttpMethod.Method, 31 | localPath.Replace("/", "-").Replace("{", String.Empty).Replace("}", String.Empty)); 32 | if (queryKeyString != null) 33 | { 34 | friendlyPath.AppendFormat("_{0}", queryKeyString.Replace('.', '-')); 35 | } 36 | return friendlyPath.ToString(); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Controllers/HelpController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Web.Http; 3 | using System.Web.Mvc; 4 | using WebApp.Areas.HelpPage.ModelDescriptions; 5 | using WebApp.Areas.HelpPage.Models; 6 | 7 | namespace WebApp.Areas.HelpPage.Controllers 8 | { 9 | /// 10 | /// The controller that will handle requests for the help page. 11 | /// 12 | public class HelpController : Controller 13 | { 14 | private const string ErrorViewName = "Error"; 15 | 16 | public HelpController() 17 | : this(GlobalConfiguration.Configuration) 18 | { 19 | } 20 | 21 | public HelpController(HttpConfiguration config) 22 | { 23 | Configuration = config; 24 | } 25 | 26 | public HttpConfiguration Configuration { get; private set; } 27 | 28 | public ActionResult Index() 29 | { 30 | ViewBag.DocumentationProvider = Configuration.Services.GetDocumentationProvider(); 31 | return View(Configuration.Services.GetApiExplorer().ApiDescriptions); 32 | } 33 | 34 | public ActionResult Api(string apiId) 35 | { 36 | if (!String.IsNullOrEmpty(apiId)) 37 | { 38 | HelpPageApiModel apiModel = Configuration.GetHelpPageApiModel(apiId); 39 | if (apiModel != null) 40 | { 41 | return View(apiModel); 42 | } 43 | } 44 | 45 | return View(ErrorViewName); 46 | } 47 | 48 | public ActionResult ResourceModel(string modelName) 49 | { 50 | if (!String.IsNullOrEmpty(modelName)) 51 | { 52 | ModelDescriptionGenerator modelDescriptionGenerator = Configuration.GetModelDescriptionGenerator(); 53 | ModelDescription modelDescription; 54 | if (modelDescriptionGenerator.GeneratedModels.TryGetValue(modelName, out modelDescription)) 55 | { 56 | return View(modelDescription); 57 | } 58 | } 59 | 60 | return View(ErrorViewName); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/HelpPage.css: -------------------------------------------------------------------------------- 1 | .help-page h1, 2 | .help-page .h1, 3 | .help-page h2, 4 | .help-page .h2, 5 | .help-page h3, 6 | .help-page .h3, 7 | #body.help-page, 8 | .help-page-table th, 9 | .help-page-table pre, 10 | .help-page-table p { 11 | font-family: "Segoe UI Light", Frutiger, "Frutiger Linotype", "Dejavu Sans", "Helvetica Neue", Arial, sans-serif; 12 | } 13 | 14 | .help-page pre.wrapped { 15 | white-space: -moz-pre-wrap; 16 | white-space: -pre-wrap; 17 | white-space: -o-pre-wrap; 18 | white-space: pre-wrap; 19 | } 20 | 21 | .help-page .warning-message-container { 22 | margin-top: 20px; 23 | padding: 0 10px; 24 | color: #525252; 25 | background: #EFDCA9; 26 | border: 1px solid #CCCCCC; 27 | } 28 | 29 | .help-page-table { 30 | width: 100%; 31 | border-collapse: collapse; 32 | text-align: left; 33 | margin: 0px 0px 20px 0px; 34 | border-top: 1px solid #D4D4D4; 35 | } 36 | 37 | .help-page-table th { 38 | text-align: left; 39 | font-weight: bold; 40 | border-bottom: 1px solid #D4D4D4; 41 | padding: 5px 6px 5px 6px; 42 | } 43 | 44 | .help-page-table td { 45 | border-bottom: 1px solid #D4D4D4; 46 | padding: 10px 8px 10px 8px; 47 | vertical-align: top; 48 | } 49 | 50 | .help-page-table pre, 51 | .help-page-table p { 52 | margin: 0px; 53 | padding: 0px; 54 | font-family: inherit; 55 | font-size: 100%; 56 | } 57 | 58 | .help-page-table tbody tr:hover td { 59 | background-color: #F3F3F3; 60 | } 61 | 62 | .help-page a:hover { 63 | background-color: transparent; 64 | } 65 | 66 | .help-page .sample-header { 67 | border: 2px solid #D4D4D4; 68 | background: #00497E; 69 | color: #FFFFFF; 70 | padding: 8px 15px; 71 | border-bottom: none; 72 | display: inline-block; 73 | margin: 10px 0px 0px 0px; 74 | } 75 | 76 | .help-page .sample-content { 77 | display: block; 78 | border-width: 0; 79 | padding: 15px 20px; 80 | background: #FFFFFF; 81 | border: 2px solid #D4D4D4; 82 | margin: 0px 0px 10px 0px; 83 | } 84 | 85 | .help-page .api-name { 86 | width: 40%; 87 | } 88 | 89 | .help-page .api-documentation { 90 | width: 60%; 91 | } 92 | 93 | .help-page .parameter-name { 94 | width: 20%; 95 | } 96 | 97 | .help-page .parameter-documentation { 98 | width: 40%; 99 | } 100 | 101 | .help-page .parameter-type { 102 | width: 20%; 103 | } 104 | 105 | .help-page .parameter-annotations { 106 | width: 20%; 107 | } 108 | 109 | .help-page h1, 110 | .help-page .h1 { 111 | font-size: 36px; 112 | line-height: normal; 113 | } 114 | 115 | .help-page h2, 116 | .help-page .h2 { 117 | font-size: 24px; 118 | } 119 | 120 | .help-page h3, 121 | .help-page .h3 { 122 | font-size: 20px; 123 | } 124 | 125 | #body.help-page { 126 | font-size: 14px; 127 | line-height: 143%; 128 | color: #333; 129 | } 130 | 131 | .help-page a { 132 | color: #0000EE; 133 | text-decoration: none; 134 | } 135 | -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/HelpPageAreaRegistration.cs: -------------------------------------------------------------------------------- 1 | using System.Web.Http; 2 | using System.Web.Mvc; 3 | 4 | namespace WebApp.Areas.HelpPage 5 | { 6 | public class HelpPageAreaRegistration : AreaRegistration 7 | { 8 | public override string AreaName 9 | { 10 | get 11 | { 12 | return "HelpPage"; 13 | } 14 | } 15 | 16 | public override void RegisterArea(AreaRegistrationContext context) 17 | { 18 | context.MapRoute( 19 | "HelpPage_Default", 20 | "Help/{action}/{apiId}", 21 | new { controller = "Help", action = "Index", apiId = UrlParameter.Optional }); 22 | 23 | HelpPageConfig.Register(GlobalConfiguration.Configuration); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/ModelDescriptions/CollectionModelDescription.cs: -------------------------------------------------------------------------------- 1 | namespace WebApp.Areas.HelpPage.ModelDescriptions 2 | { 3 | public class CollectionModelDescription : ModelDescription 4 | { 5 | public ModelDescription ElementDescription { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/ModelDescriptions/ComplexTypeModelDescription.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace WebApp.Areas.HelpPage.ModelDescriptions 4 | { 5 | public class ComplexTypeModelDescription : ModelDescription 6 | { 7 | public ComplexTypeModelDescription() 8 | { 9 | Properties = new Collection(); 10 | } 11 | 12 | public Collection Properties { get; private set; } 13 | } 14 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/ModelDescriptions/DictionaryModelDescription.cs: -------------------------------------------------------------------------------- 1 | namespace WebApp.Areas.HelpPage.ModelDescriptions 2 | { 3 | public class DictionaryModelDescription : KeyValuePairModelDescription 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/ModelDescriptions/EnumTypeModelDescription.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | 4 | namespace WebApp.Areas.HelpPage.ModelDescriptions 5 | { 6 | public class EnumTypeModelDescription : ModelDescription 7 | { 8 | public EnumTypeModelDescription() 9 | { 10 | Values = new Collection(); 11 | } 12 | 13 | public Collection Values { get; private set; } 14 | } 15 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/ModelDescriptions/EnumValueDescription.cs: -------------------------------------------------------------------------------- 1 | namespace WebApp.Areas.HelpPage.ModelDescriptions 2 | { 3 | public class EnumValueDescription 4 | { 5 | public string Documentation { get; set; } 6 | 7 | public string Name { get; set; } 8 | 9 | public string Value { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/ModelDescriptions/IModelDocumentationProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace WebApp.Areas.HelpPage.ModelDescriptions 5 | { 6 | public interface IModelDocumentationProvider 7 | { 8 | string GetDocumentation(MemberInfo member); 9 | 10 | string GetDocumentation(Type type); 11 | } 12 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/ModelDescriptions/KeyValuePairModelDescription.cs: -------------------------------------------------------------------------------- 1 | namespace WebApp.Areas.HelpPage.ModelDescriptions 2 | { 3 | public class KeyValuePairModelDescription : ModelDescription 4 | { 5 | public ModelDescription KeyModelDescription { get; set; } 6 | 7 | public ModelDescription ValueModelDescription { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/ModelDescriptions/ModelDescription.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApp.Areas.HelpPage.ModelDescriptions 4 | { 5 | /// 6 | /// Describes a type model. 7 | /// 8 | public abstract class ModelDescription 9 | { 10 | public string Documentation { get; set; } 11 | 12 | public Type ModelType { get; set; } 13 | 14 | public string Name { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/ModelDescriptions/ModelNameAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApp.Areas.HelpPage.ModelDescriptions 4 | { 5 | /// 6 | /// Use this attribute to change the name of the generated for a type. 7 | /// 8 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = false, Inherited = false)] 9 | public sealed class ModelNameAttribute : Attribute 10 | { 11 | public ModelNameAttribute(string name) 12 | { 13 | Name = name; 14 | } 15 | 16 | public string Name { get; private set; } 17 | } 18 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/ModelDescriptions/ModelNameHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace WebApp.Areas.HelpPage.ModelDescriptions 7 | { 8 | internal static class ModelNameHelper 9 | { 10 | // Modify this to provide custom model name mapping. 11 | public static string GetModelName(Type type) 12 | { 13 | ModelNameAttribute modelNameAttribute = type.GetCustomAttribute(); 14 | if (modelNameAttribute != null && !String.IsNullOrEmpty(modelNameAttribute.Name)) 15 | { 16 | return modelNameAttribute.Name; 17 | } 18 | 19 | string modelName = type.Name; 20 | if (type.IsGenericType) 21 | { 22 | // Format the generic type name to something like: GenericOfAgurment1AndArgument2 23 | Type genericType = type.GetGenericTypeDefinition(); 24 | Type[] genericArguments = type.GetGenericArguments(); 25 | string genericTypeName = genericType.Name; 26 | 27 | // Trim the generic parameter counts from the name 28 | genericTypeName = genericTypeName.Substring(0, genericTypeName.IndexOf('`')); 29 | string[] argumentTypeNames = genericArguments.Select(t => GetModelName(t)).ToArray(); 30 | modelName = String.Format(CultureInfo.InvariantCulture, "{0}Of{1}", genericTypeName, String.Join("And", argumentTypeNames)); 31 | } 32 | 33 | return modelName; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/ModelDescriptions/ParameterAnnotation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApp.Areas.HelpPage.ModelDescriptions 4 | { 5 | public class ParameterAnnotation 6 | { 7 | public Attribute AnnotationAttribute { get; set; } 8 | 9 | public string Documentation { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/ModelDescriptions/ParameterDescription.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | 4 | namespace WebApp.Areas.HelpPage.ModelDescriptions 5 | { 6 | public class ParameterDescription 7 | { 8 | public ParameterDescription() 9 | { 10 | Annotations = new Collection(); 11 | } 12 | 13 | public Collection Annotations { get; private set; } 14 | 15 | public string Documentation { get; set; } 16 | 17 | public string Name { get; set; } 18 | 19 | public ModelDescription TypeDescription { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/ModelDescriptions/SimpleTypeModelDescription.cs: -------------------------------------------------------------------------------- 1 | namespace WebApp.Areas.HelpPage.ModelDescriptions 2 | { 3 | public class SimpleTypeModelDescription : ModelDescription 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/SampleGeneration/ImageSample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApp.Areas.HelpPage 4 | { 5 | /// 6 | /// This represents an image sample on the help page. There's a display template named ImageSample associated with this class. 7 | /// 8 | public class ImageSample 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// The URL of an image. 14 | public ImageSample(string src) 15 | { 16 | if (src == null) 17 | { 18 | throw new ArgumentNullException("src"); 19 | } 20 | Src = src; 21 | } 22 | 23 | public string Src { get; private set; } 24 | 25 | public override bool Equals(object obj) 26 | { 27 | ImageSample other = obj as ImageSample; 28 | return other != null && Src == other.Src; 29 | } 30 | 31 | public override int GetHashCode() 32 | { 33 | return Src.GetHashCode(); 34 | } 35 | 36 | public override string ToString() 37 | { 38 | return Src; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/SampleGeneration/InvalidSample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApp.Areas.HelpPage 4 | { 5 | /// 6 | /// This represents an invalid sample on the help page. There's a display template named InvalidSample associated with this class. 7 | /// 8 | public class InvalidSample 9 | { 10 | public InvalidSample(string errorMessage) 11 | { 12 | if (errorMessage == null) 13 | { 14 | throw new ArgumentNullException("errorMessage"); 15 | } 16 | ErrorMessage = errorMessage; 17 | } 18 | 19 | public string ErrorMessage { get; private set; } 20 | 21 | public override bool Equals(object obj) 22 | { 23 | InvalidSample other = obj as InvalidSample; 24 | return other != null && ErrorMessage == other.ErrorMessage; 25 | } 26 | 27 | public override int GetHashCode() 28 | { 29 | return ErrorMessage.GetHashCode(); 30 | } 31 | 32 | public override string ToString() 33 | { 34 | return ErrorMessage; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/SampleGeneration/SampleDirection.cs: -------------------------------------------------------------------------------- 1 | namespace WebApp.Areas.HelpPage 2 | { 3 | /// 4 | /// Indicates whether the sample is used for request or response 5 | /// 6 | public enum SampleDirection 7 | { 8 | Request = 0, 9 | Response 10 | } 11 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/SampleGeneration/TextSample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApp.Areas.HelpPage 4 | { 5 | /// 6 | /// This represents a preformatted text sample on the help page. There's a display template named TextSample associated with this class. 7 | /// 8 | public class TextSample 9 | { 10 | public TextSample(string text) 11 | { 12 | if (text == null) 13 | { 14 | throw new ArgumentNullException("text"); 15 | } 16 | Text = text; 17 | } 18 | 19 | public string Text { get; private set; } 20 | 21 | public override bool Equals(object obj) 22 | { 23 | TextSample other = obj as TextSample; 24 | return other != null && Text == other.Text; 25 | } 26 | 27 | public override int GetHashCode() 28 | { 29 | return Text.GetHashCode(); 30 | } 31 | 32 | public override string ToString() 33 | { 34 | return Text; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/Api.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Web.Http 2 | @using WebApp.Areas.HelpPage.Models 3 | @model HelpPageApiModel 4 | 5 | @{ 6 | var description = Model.ApiDescription; 7 | ViewBag.Title = description.HttpMethod.Method + " " + description.RelativePath; 8 | } 9 | 10 | 11 |
12 | 19 |
20 | @Html.DisplayForModel() 21 |
22 |
23 | -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/DisplayTemplates/ApiGroup.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Web.Http 2 | @using System.Web.Http.Controllers 3 | @using System.Web.Http.Description 4 | @using WebApp.Areas.HelpPage 5 | @using WebApp.Areas.HelpPage.Models 6 | @model IGrouping 7 | 8 | @{ 9 | var controllerDocumentation = ViewBag.DocumentationProvider != null ? 10 | ViewBag.DocumentationProvider.GetDocumentation(Model.Key) : 11 | null; 12 | } 13 | 14 |

@Model.Key.ControllerName

15 | @if (!String.IsNullOrEmpty(controllerDocumentation)) 16 | { 17 |

@controllerDocumentation

18 | } 19 | 20 | 21 | 22 | 23 | 24 | @foreach (var api in Model) 25 | { 26 | 27 | 28 | 38 | 39 | } 40 | 41 |
APIDescription
@api.HttpMethod.Method @api.RelativePath 29 | @if (api.Documentation != null) 30 | { 31 |

@api.Documentation

32 | } 33 | else 34 | { 35 |

No documentation available.

36 | } 37 |
-------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/DisplayTemplates/CollectionModelDescription.cshtml: -------------------------------------------------------------------------------- 1 | @using WebApp.Areas.HelpPage.ModelDescriptions 2 | @model CollectionModelDescription 3 | @if (Model.ElementDescription is ComplexTypeModelDescription) 4 | { 5 | @Html.DisplayFor(m => m.ElementDescription) 6 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/DisplayTemplates/ComplexTypeModelDescription.cshtml: -------------------------------------------------------------------------------- 1 | @using WebApp.Areas.HelpPage.ModelDescriptions 2 | @model ComplexTypeModelDescription 3 | @Html.DisplayFor(m => m.Properties, "Parameters") -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/DisplayTemplates/DictionaryModelDescription.cshtml: -------------------------------------------------------------------------------- 1 | @using WebApp.Areas.HelpPage.ModelDescriptions 2 | @model DictionaryModelDescription 3 | Dictionary of @Html.DisplayFor(m => Model.KeyModelDescription.ModelType, "ModelDescriptionLink", new { modelDescription = Model.KeyModelDescription }) [key] 4 | and @Html.DisplayFor(m => Model.ValueModelDescription.ModelType, "ModelDescriptionLink", new { modelDescription = Model.ValueModelDescription }) [value] -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/DisplayTemplates/EnumTypeModelDescription.cshtml: -------------------------------------------------------------------------------- 1 | @using WebApp.Areas.HelpPage.ModelDescriptions 2 | @model EnumTypeModelDescription 3 | 4 |

Possible enumeration values:

5 | 6 | 7 | 8 | 9 | 10 | 11 | @foreach (EnumValueDescription value in Model.Values) 12 | { 13 | 14 | 15 | 18 | 21 | 22 | } 23 | 24 |
NameValueDescription
@value.Name 16 |

@value.Value

17 |
19 |

@value.Documentation

20 |
-------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/DisplayTemplates/HelpPageApiModel.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Web.Http 2 | @using System.Web.Http.Description 3 | @using WebApp.Areas.HelpPage.Models 4 | @using WebApp.Areas.HelpPage.ModelDescriptions 5 | @model HelpPageApiModel 6 | 7 | @{ 8 | ApiDescription description = Model.ApiDescription; 9 | } 10 |

@description.HttpMethod.Method @description.RelativePath

11 |
12 |

@description.Documentation

13 | 14 |

Request Information

15 | 16 |

URI Parameters

17 | @Html.DisplayFor(m => m.UriParameters, "Parameters") 18 | 19 |

Body Parameters

20 | 21 |

@Model.RequestDocumentation

22 | 23 | @if (Model.RequestModelDescription != null) 24 | { 25 | @Html.DisplayFor(m => m.RequestModelDescription.ModelType, "ModelDescriptionLink", new { modelDescription = Model.RequestModelDescription }) 26 | if (Model.RequestBodyParameters != null) 27 | { 28 | @Html.DisplayFor(m => m.RequestBodyParameters, "Parameters") 29 | } 30 | } 31 | else 32 | { 33 |

None.

34 | } 35 | 36 | @if (Model.SampleRequests.Count > 0) 37 | { 38 |

Request Formats

39 | @Html.DisplayFor(m => m.SampleRequests, "Samples") 40 | } 41 | 42 |

Response Information

43 | 44 |

Resource Description

45 | 46 |

@description.ResponseDescription.Documentation

47 | 48 | @if (Model.ResourceDescription != null) 49 | { 50 | @Html.DisplayFor(m => m.ResourceDescription.ModelType, "ModelDescriptionLink", new { modelDescription = Model.ResourceDescription }) 51 | if (Model.ResourceProperties != null) 52 | { 53 | @Html.DisplayFor(m => m.ResourceProperties, "Parameters") 54 | } 55 | } 56 | else 57 | { 58 |

None.

59 | } 60 | 61 | @if (Model.SampleResponses.Count > 0) 62 | { 63 |

Response Formats

64 | @Html.DisplayFor(m => m.SampleResponses, "Samples") 65 | } 66 | 67 |
-------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/DisplayTemplates/ImageSample.cshtml: -------------------------------------------------------------------------------- 1 | @using WebApp.Areas.HelpPage 2 | @model ImageSample 3 | 4 | -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/DisplayTemplates/InvalidSample.cshtml: -------------------------------------------------------------------------------- 1 | @using WebApp.Areas.HelpPage 2 | @model InvalidSample 3 | 4 | @if (HttpContext.Current.IsDebuggingEnabled) 5 | { 6 |
7 |

@Model.ErrorMessage

8 |
9 | } 10 | else 11 | { 12 |

Sample not available.

13 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/DisplayTemplates/KeyValuePairModelDescription.cshtml: -------------------------------------------------------------------------------- 1 | @using WebApp.Areas.HelpPage.ModelDescriptions 2 | @model KeyValuePairModelDescription 3 | Pair of @Html.DisplayFor(m => Model.KeyModelDescription.ModelType, "ModelDescriptionLink", new { modelDescription = Model.KeyModelDescription }) [key] 4 | and @Html.DisplayFor(m => Model.ValueModelDescription.ModelType, "ModelDescriptionLink", new { modelDescription = Model.ValueModelDescription }) [value] -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/DisplayTemplates/ModelDescriptionLink.cshtml: -------------------------------------------------------------------------------- 1 | @using WebApp.Areas.HelpPage.ModelDescriptions 2 | @model Type 3 | @{ 4 | ModelDescription modelDescription = ViewBag.modelDescription; 5 | if (modelDescription is ComplexTypeModelDescription || modelDescription is EnumTypeModelDescription) 6 | { 7 | if (Model == typeof(Object)) 8 | { 9 | @:Object 10 | } 11 | else 12 | { 13 | @Html.ActionLink(modelDescription.Name, "ResourceModel", "Help", new { modelName = modelDescription.Name }, null) 14 | } 15 | } 16 | else if (modelDescription is CollectionModelDescription) 17 | { 18 | var collectionDescription = modelDescription as CollectionModelDescription; 19 | var elementDescription = collectionDescription.ElementDescription; 20 | @:Collection of @Html.DisplayFor(m => elementDescription.ModelType, "ModelDescriptionLink", new { modelDescription = elementDescription }) 21 | } 22 | else 23 | { 24 | @Html.DisplayFor(m => modelDescription) 25 | } 26 | } -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/DisplayTemplates/Parameters.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Collections.Generic 2 | @using System.Collections.ObjectModel 3 | @using System.Web.Http.Description 4 | @using System.Threading 5 | @using WebApp.Areas.HelpPage.ModelDescriptions 6 | @model IList 7 | 8 | @if (Model.Count > 0) 9 | { 10 | 11 | 12 | 13 | 14 | 15 | @foreach (ParameterDescription parameter in Model) 16 | { 17 | ModelDescription modelDescription = parameter.TypeDescription; 18 | 19 | 20 | 23 | 26 | 39 | 40 | } 41 | 42 |
NameDescriptionTypeAdditional information
@parameter.Name 21 |

@parameter.Documentation

22 |
24 | @Html.DisplayFor(m => modelDescription.ModelType, "ModelDescriptionLink", new { modelDescription = modelDescription }) 25 | 27 | @if (parameter.Annotations.Count > 0) 28 | { 29 | foreach (var annotation in parameter.Annotations) 30 | { 31 |

@annotation.Documentation

32 | } 33 | } 34 | else 35 | { 36 |

None.

37 | } 38 |
43 | } 44 | else 45 | { 46 |

None.

47 | } 48 | 49 | -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/DisplayTemplates/Samples.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Net.Http.Headers 2 | @model Dictionary 3 | 4 | @{ 5 | // Group the samples into a single tab if they are the same. 6 | Dictionary samples = Model.GroupBy(pair => pair.Value).ToDictionary( 7 | pair => String.Join(", ", pair.Select(m => m.Key.ToString()).ToArray()), 8 | pair => pair.Key); 9 | var mediaTypes = samples.Keys; 10 | } 11 |
12 | @foreach (var mediaType in mediaTypes) 13 | { 14 |

@mediaType

15 |
16 | Sample: 17 | @{ 18 | var sample = samples[mediaType]; 19 | if (sample == null) 20 | { 21 |

Sample not available.

22 | } 23 | else 24 | { 25 | @Html.DisplayFor(s => sample); 26 | } 27 | } 28 |
29 | } 30 |
-------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/DisplayTemplates/SimpleTypeModelDescription.cshtml: -------------------------------------------------------------------------------- 1 | @using WebApp.Areas.HelpPage.ModelDescriptions 2 | @model SimpleTypeModelDescription 3 | @Model.Documentation -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/DisplayTemplates/TextSample.cshtml: -------------------------------------------------------------------------------- 1 | @using WebApp.Areas.HelpPage 2 | @model TextSample 3 | 4 |
5 | @Model.Text
6 | 
-------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/Index.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Web.Http 2 | @using System.Web.Http.Controllers 3 | @using System.Web.Http.Description 4 | @using System.Collections.ObjectModel 5 | @using WebApp.Areas.HelpPage.Models 6 | @model Collection 7 | 8 | @{ 9 | ViewBag.Title = "ASP.NET Web API Help Page"; 10 | 11 | // Group APIs by controller 12 | ILookup apiGroups = Model.ToLookup(api => api.ActionDescriptor.ControllerDescriptor); 13 | } 14 | 15 | 16 |
17 |
18 |
19 |

@ViewBag.Title

20 |
21 |
22 |
23 |
24 | 32 |
33 | @foreach (var group in apiGroups) 34 | { 35 | @Html.DisplayFor(m => group, "ApiGroup") 36 | } 37 |
38 |
39 | -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Help/ResourceModel.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Web.Http 2 | @using WebApp.Areas.HelpPage.ModelDescriptions 3 | @model ModelDescription 4 | 5 | 6 |
7 | 14 |

@Model.Name

15 |

@Model.Documentation

16 |
17 | @Html.DisplayFor(m => Model) 18 |
19 |
20 | -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @ViewBag.Title 7 | @RenderSection("scripts", required: false) 8 | 9 | 10 | @RenderBody() 11 | 12 | -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/Web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /samples/WebApp/Areas/HelpPage/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | // Change the Layout path below to blend the look and feel of the help page with your existing web pages 3 | Layout = "~/Views/Shared/_Layout.cshtml"; 4 | } -------------------------------------------------------------------------------- /samples/WebApp/Content/Site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Set padding to keep content from hitting the edges */ 7 | .body-content { 8 | padding-left: 15px; 9 | padding-right: 15px; 10 | } 11 | 12 | /* Set width on the form input elements since they're 100% wide by default */ 13 | input, 14 | select, 15 | textarea { 16 | max-width: 280px; 17 | } 18 | -------------------------------------------------------------------------------- /samples/WebApp/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System.Web.Mvc; 2 | using Unleash; 3 | 4 | namespace WebApp.Controllers 5 | { 6 | public class HomeController : Controller 7 | { 8 | private readonly IUnleash unleash; 9 | 10 | public HomeController() 11 | { 12 | unleash = WebApiApplication.Unleash; 13 | } 14 | 15 | public ActionResult Index() 16 | { 17 | ViewBag.Title = "Unleash"; 18 | 19 | ViewBag.Feature = "Demo123"; 20 | ViewBag.FeatureEnabled = unleash.IsEnabled(ViewBag.Feature); 21 | 22 | return View(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/WebApp/Controllers/ValuesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Web.Http; 7 | 8 | namespace WebApp.Controllers 9 | { 10 | public class ValuesController : ApiController 11 | { 12 | // GET api/values 13 | public IEnumerable Get() 14 | { 15 | return new string[] { "value1", "value2" }; 16 | } 17 | 18 | // GET api/values/5 19 | public string Get(int id) 20 | { 21 | return "value"; 22 | } 23 | 24 | // POST api/values 25 | public void Post([FromBody]string value) 26 | { 27 | } 28 | 29 | // PUT api/values/5 30 | public void Put(int id, [FromBody]string value) 31 | { 32 | } 33 | 34 | // DELETE api/values/5 35 | public void Delete(int id) 36 | { 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /samples/WebApp/Global.asax: -------------------------------------------------------------------------------- 1 | <%@ Application Codebehind="Global.asax.cs" Inherits="WebApp.WebApiApplication" Language="C#" %> 2 | -------------------------------------------------------------------------------- /samples/WebApp/Global.asax.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Web; 4 | using System.Web.Http; 5 | using System.Web.Mvc; 6 | using System.Web.Optimization; 7 | using System.Web.Routing; 8 | using Unleash; 9 | 10 | namespace WebApp 11 | { 12 | public class WebApiApplication : HttpApplication 13 | { 14 | public static IUnleash Unleash { get; private set; } 15 | public static UnleashSettings UnleashSettings { get; private set; } 16 | 17 | protected void Application_Start() 18 | { 19 | AreaRegistration.RegisterAllAreas(); 20 | GlobalConfiguration.Configure(WebApiConfig.Register); 21 | FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); 22 | RouteConfig.RegisterRoutes(RouteTable.Routes); 23 | BundleConfig.RegisterBundles(BundleTable.Bundles); 24 | 25 | UnleashSettings = new UnleashSettings() 26 | { 27 | UnleashApi = new Uri("http://unleash.herokuapp.com/api/"), 28 | //UnleashApi = new Uri("http://localhost:4242/api/"), 29 | AppName = "dotnet-api-test", 30 | InstanceTag = "instance 1", 31 | SendMetricsInterval = TimeSpan.FromSeconds(20), 32 | UnleashContextProvider = new AspNetContextProvider(), 33 | //JsonSerializer = new JsonNetSerializer() 34 | }; 35 | 36 | /* Uncomment to test bootstrapping via url. This particular url points to the BootstapHost, a thin webapp hosting a bootstrap.json file 37 | UnleashSettings.BootstrapOverride = true; 38 | UnleashSettings.UseBootstrapUrlProvider("http://localhost:41028/bootstrap.json", true); 39 | */ 40 | UnleashInfo = UnleashSettings.ToString(); 41 | 42 | Unleash = new DefaultUnleash(UnleashSettings); 43 | } 44 | 45 | public static string UnleashInfo; 46 | 47 | protected void Application_End() 48 | { 49 | Unleash.Dispose(); 50 | } 51 | 52 | protected void Application_BeginRequest(object sender, EventArgs e) 53 | { 54 | HttpContext.Current.Items["UnleashContext"] = new UnleashContext 55 | { 56 | UserId = "ABC", 57 | SessionId = HttpContext.Current.Session?.SessionID, 58 | RemoteAddress = HttpContext.Current.Request.UserHostAddress, 59 | Properties = new Dictionary() 60 | { 61 | {"UserRoles", "A, B, C"} 62 | } 63 | }; 64 | } 65 | } 66 | 67 | public class AspNetContextProvider : IUnleashContextProvider 68 | { 69 | public UnleashContext Context => 70 | HttpContext.Current?.Items["UnleashContext"] as UnleashContext; 71 | } 72 | } -------------------------------------------------------------------------------- /samples/WebApp/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("WebApp")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("WebApp")] 13 | [assembly: AssemblyCopyright("Copyright © 2017")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("7dd13ae2-9dcd-40ac-8c7b-9af4f2268d25")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Revision and Build Numbers 33 | // by using the '*' as shown below: 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /samples/WebApp/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | 
2 |

Unleash

3 |

Active?

4 |

@ViewBag.Feature: @ViewBag.FeatureEnabled

5 |
6 |

Learn more »

7 |
-------------------------------------------------------------------------------- /samples/WebApp/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Error 6 | 7 | 8 |
9 |

Error.

10 |

An error occurred while processing your request.

11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/WebApp/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewBag.Title 7 | @Styles.Render("~/Content/css") 8 | @Scripts.Render("~/bundles/modernizr") 9 | 10 | 11 | 12 | 13 | 30 |
31 | @RenderBody() 32 |
33 |

@WebApiApplication.UnleashInfo

34 |
35 |

© @DateTime.Now.Year - My ASP.NET Application

36 |
37 |
38 | 39 | @Scripts.Render("~/bundles/jquery") 40 | @Scripts.Render("~/bundles/bootstrap") 41 | @RenderSection("scripts", required: false) 42 | 43 | 44 | -------------------------------------------------------------------------------- /samples/WebApp/Views/Web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /samples/WebApp/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "~/Views/Shared/_Layout.cshtml"; 3 | } 4 | -------------------------------------------------------------------------------- /samples/WebApp/Web.Debug.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 17 | 18 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /samples/WebApp/Web.Release.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 17 | 18 | 19 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /samples/WebApp/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-dotnet-sdk/8094dfe89d20e82c51f9ffb8b2570d0c931fe4b7/samples/WebApp/favicon.ico -------------------------------------------------------------------------------- /samples/WebApp/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-dotnet-sdk/8094dfe89d20e82c51f9ffb8b2570d0c931fe4b7/samples/WebApp/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /samples/WebApp/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-dotnet-sdk/8094dfe89d20e82c51f9ffb8b2570d0c931fe4b7/samples/WebApp/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /samples/WebApp/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-dotnet-sdk/8094dfe89d20e82c51f9ffb8b2570d0c931fe4b7/samples/WebApp/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /samples/WebApp/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /samples/WebAppCore/Pages/About.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model AboutModel 3 | @{ 4 | ViewData["Title"] = "About"; 5 | } 6 |

@ViewData["Title"]

7 |

@Model.Message

8 | 9 |

Use this area to provide additional information.

10 | -------------------------------------------------------------------------------- /samples/WebAppCore/Pages/About.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | 7 | namespace WebAppCore.Pages 8 | { 9 | public class AboutModel : PageModel 10 | { 11 | public string Message { get; set; } 12 | 13 | public void OnGet() 14 | { 15 | Message = "Your application description page."; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /samples/WebAppCore/Pages/Contact.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ContactModel 3 | @{ 4 | ViewData["Title"] = "Contact"; 5 | } 6 |

@ViewData["Title"]

7 |

@Model.Message

8 | 9 |
10 | One Microsoft Way
11 | Redmond, WA 98052-6399
12 | P: 13 | 425.555.0100 14 |
15 | 16 |
17 | Support: Support@example.com
18 | Marketing: Marketing@example.com 19 |
20 | -------------------------------------------------------------------------------- /samples/WebAppCore/Pages/Contact.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | 7 | namespace WebAppCore.Pages 8 | { 9 | public class ContactModel : PageModel 10 | { 11 | public string Message { get; set; } 12 | 13 | public void OnGet() 14 | { 15 | Message = "Your contact page."; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /samples/WebAppCore/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ErrorModel 3 | @{ 4 | ViewData["Title"] = "Error"; 5 | } 6 | 7 |

Error.

8 |

An error occurred while processing your request.

9 | 10 | @if (Model.ShowRequestId) 11 | { 12 |

13 | Request ID: @Model.RequestId 14 |

15 | } 16 | 17 |

Development Mode

18 |

19 | Swapping to Development environment will display more detailed information about the error that occurred. 20 |

21 |

22 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 23 |

24 | -------------------------------------------------------------------------------- /samples/WebAppCore/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | 8 | namespace WebAppCore.Pages 9 | { 10 | public class ErrorModel : PageModel 11 | { 12 | public string RequestId { get; set; } 13 | 14 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 15 | 16 | public void OnGet() 17 | { 18 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/WebAppCore/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | 8 | namespace WebAppCore.Pages 9 | { 10 | public class IndexModel : PageModel 11 | { 12 | public void OnGet() 13 | { 14 | 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /samples/WebAppCore/Pages/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - WebAppCore 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 40 |
41 | @RenderBody() 42 |
43 |
44 |

© 2017 - WebAppCore

45 |
46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 60 | 66 | 67 | 68 | 69 | @RenderSection("Scripts", required: false) 70 | 71 | 72 | -------------------------------------------------------------------------------- /samples/WebAppCore/Pages/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /samples/WebAppCore/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using WebAppCore 2 | @namespace WebAppCore.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /samples/WebAppCore/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /samples/WebAppCore/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace WebAppCore 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | BuildWebHost(args).Run(); 18 | } 19 | 20 | public static IWebHost BuildWebHost(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup() 23 | .Build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/WebAppCore/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:53118/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "WebAppCore": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:53119/" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /samples/WebAppCore/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | namespace WebAppCore 11 | { 12 | public class Startup 13 | { 14 | public Startup(IConfiguration configuration) 15 | { 16 | Configuration = configuration; 17 | } 18 | 19 | public IConfiguration Configuration { get; } 20 | 21 | // This method gets called by the runtime. Use this method to add services to the container. 22 | public void ConfigureServices(IServiceCollection services) 23 | { 24 | services.AddMvc(); 25 | } 26 | 27 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 28 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 29 | { 30 | if (env.IsDevelopment()) 31 | { 32 | app.UseDeveloperExceptionPage(); 33 | app.UseBrowserLink(); 34 | } 35 | else 36 | { 37 | app.UseExceptionHandler("/Error"); 38 | } 39 | 40 | app.UseStaticFiles(); 41 | 42 | app.UseMvc(routes => 43 | { 44 | routes.MapRoute( 45 | name: "default", 46 | template: "{controller}/{action=Index}/{id?}"); 47 | }); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /samples/WebAppCore/WebAppCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/WebAppCore/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/WebAppCore/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /samples/WebAppCore/bundleconfig.json: -------------------------------------------------------------------------------- 1 | // Configure bundling and minification for the project. 2 | // More info at https://go.microsoft.com/fwlink/?LinkId=808241 3 | [ 4 | { 5 | "outputFileName": "wwwroot/css/site.min.css", 6 | // An array of relative input file paths. Globbing patterns supported 7 | "inputFiles": [ 8 | "wwwroot/css/site.css" 9 | ] 10 | }, 11 | { 12 | "outputFileName": "wwwroot/js/site.min.js", 13 | "inputFiles": [ 14 | "wwwroot/js/site.js" 15 | ], 16 | // Optionally specify minification options 17 | "minify": { 18 | "enabled": true, 19 | "renameLocals": true 20 | }, 21 | // Optionally generate .map file 22 | "sourceMap": false 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Wrapping element */ 7 | /* Set some basic padding to keep content from hitting the edges */ 8 | .body-content { 9 | padding-left: 15px; 10 | padding-right: 15px; 11 | } 12 | 13 | /* Carousel */ 14 | .carousel-caption p { 15 | font-size: 20px; 16 | line-height: 1.4; 17 | } 18 | 19 | /* Make .svg files in the carousel display properly in older browsers */ 20 | .carousel-inner .item img[src$=".svg"] { 21 | width: 100%; 22 | } 23 | 24 | /* QR code generator */ 25 | #qrCode { 26 | margin: 15px; 27 | } 28 | 29 | /* Hide/rearrange for smaller screens */ 30 | @media screen and (max-width: 767px) { 31 | /* Hide captions */ 32 | .carousel-caption { 33 | display: none; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}.carousel-caption p{font-size:20px;line-height:1.4}.carousel-inner .item img[src$=".svg"]{width:100%}#qrCode{margin:15px}@media screen and (max-width:767px){.carousel-caption{display:none}} -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-dotnet-sdk/8094dfe89d20e82c51f9ffb8b2570d0c931fe4b7/samples/WebAppCore/wwwroot/favicon.ico -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Write your Javascript code. 2 | -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/js/site.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-dotnet-sdk/8094dfe89d20e82c51f9ffb8b2570d0c931fe4b7/samples/WebAppCore/wwwroot/js/site.min.js -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/lib/bootstrap/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap", 3 | "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.", 4 | "keywords": [ 5 | "css", 6 | "js", 7 | "less", 8 | "mobile-first", 9 | "responsive", 10 | "front-end", 11 | "framework", 12 | "web" 13 | ], 14 | "homepage": "http://getbootstrap.com", 15 | "license": "MIT", 16 | "moduleType": "globals", 17 | "main": [ 18 | "less/bootstrap.less", 19 | "dist/js/bootstrap.js" 20 | ], 21 | "ignore": [ 22 | "/.*", 23 | "_config.yml", 24 | "CNAME", 25 | "composer.json", 26 | "CONTRIBUTING.md", 27 | "docs", 28 | "js/tests", 29 | "test-infra" 30 | ], 31 | "dependencies": { 32 | "jquery": "1.9.1 - 3" 33 | }, 34 | "version": "3.3.7", 35 | "_release": "3.3.7", 36 | "_resolution": { 37 | "type": "version", 38 | "tag": "v3.3.7", 39 | "commit": "0b9c4a4007c44201dce9a6cc1a38407005c26c86" 40 | }, 41 | "_source": "https://github.com/twbs/bootstrap.git", 42 | "_target": "v3.3.7", 43 | "_originalSource": "bootstrap", 44 | "_direct": true 45 | } -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/lib/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2016 Twitter, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-dotnet-sdk/8094dfe89d20e82c51f9ffb8b2570d0c931fe4b7/samples/WebAppCore/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-dotnet-sdk/8094dfe89d20e82c51f9ffb8b2570d0c931fe4b7/samples/WebAppCore/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-dotnet-sdk/8094dfe89d20e82c51f9ffb8b2570d0c931fe4b7/samples/WebAppCore/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-dotnet-sdk/8094dfe89d20e82c51f9ffb8b2570d0c931fe4b7/samples/WebAppCore/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/lib/bootstrap/dist/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/lib/jquery-validation-unobtrusive/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-validation-unobtrusive", 3 | "version": "3.2.6", 4 | "homepage": "https://github.com/aspnet/jquery-validation-unobtrusive", 5 | "description": "Add-on to jQuery Validation to enable unobtrusive validation options in data-* attributes.", 6 | "main": [ 7 | "jquery.validate.unobtrusive.js" 8 | ], 9 | "ignore": [ 10 | "**/.*", 11 | "*.json", 12 | "*.md", 13 | "*.txt", 14 | "gulpfile.js" 15 | ], 16 | "keywords": [ 17 | "jquery", 18 | "asp.net", 19 | "mvc", 20 | "validation", 21 | "unobtrusive" 22 | ], 23 | "authors": [ 24 | "Microsoft" 25 | ], 26 | "license": "http://www.microsoft.com/web/webpi/eula/net_library_eula_enu.htm", 27 | "repository": { 28 | "type": "git", 29 | "url": "git://github.com/aspnet/jquery-validation-unobtrusive.git" 30 | }, 31 | "dependencies": { 32 | "jquery-validation": ">=1.8", 33 | "jquery": ">=1.8" 34 | }, 35 | "_release": "3.2.6", 36 | "_resolution": { 37 | "type": "version", 38 | "tag": "v3.2.6", 39 | "commit": "13386cd1b5947d8a5d23a12b531ce3960be1eba7" 40 | }, 41 | "_source": "git://github.com/aspnet/jquery-validation-unobtrusive.git", 42 | "_target": "3.2.6", 43 | "_originalSource": "jquery-validation-unobtrusive" 44 | } -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/lib/jquery-validation/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-validation", 3 | "homepage": "http://jqueryvalidation.org/", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/jzaefferer/jquery-validation.git" 7 | }, 8 | "authors": [ 9 | "Jörn Zaefferer " 10 | ], 11 | "description": "Form validation made easy", 12 | "main": "dist/jquery.validate.js", 13 | "keywords": [ 14 | "forms", 15 | "validation", 16 | "validate" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "test", 24 | "demo", 25 | "lib" 26 | ], 27 | "dependencies": { 28 | "jquery": ">= 1.7.2" 29 | }, 30 | "version": "1.14.0", 31 | "_release": "1.14.0", 32 | "_resolution": { 33 | "type": "version", 34 | "tag": "1.14.0", 35 | "commit": "c1343fb9823392aa9acbe1c3ffd337b8c92fed48" 36 | }, 37 | "_source": "git://github.com/jzaefferer/jquery-validation.git", 38 | "_target": ">=1.8", 39 | "_originalSource": "jquery-validation" 40 | } -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/lib/jquery-validation/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright Jörn Zaefferer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/lib/jquery/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery", 3 | "main": "dist/jquery.js", 4 | "license": "MIT", 5 | "ignore": [ 6 | "package.json" 7 | ], 8 | "keywords": [ 9 | "jquery", 10 | "javascript", 11 | "browser", 12 | "library" 13 | ], 14 | "homepage": "https://github.com/jquery/jquery-dist", 15 | "version": "2.2.0", 16 | "_release": "2.2.0", 17 | "_resolution": { 18 | "type": "version", 19 | "tag": "2.2.0", 20 | "commit": "6fc01e29bdad0964f62ef56d01297039cdcadbe5" 21 | }, 22 | "_source": "git://github.com/jquery/jquery-dist.git", 23 | "_target": "2.2.0", 24 | "_originalSource": "jquery" 25 | } -------------------------------------------------------------------------------- /samples/WebAppCore/wwwroot/lib/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright jQuery Foundation and other contributors, https://jquery.org/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /samples/WebApplication/Controllers/UnleashController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Logging; 5 | using Unleash; 6 | using Unleash.Internal; 7 | using Unleash.Variants; 8 | 9 | namespace WebApplication.Controllers 10 | { 11 | [ApiController] 12 | [Route("[controller]")] 13 | public class UnleashController : ControllerBase 14 | { 15 | private readonly ILogger _logger; 16 | private readonly IUnleash _unleash; 17 | 18 | public UnleashController(ILogger logger, IUnleash unleash) 19 | { 20 | _logger = logger; 21 | _unleash = unleash; 22 | } 23 | 24 | [HttpGet("status")] 25 | public string GetStatus() 26 | { 27 | _logger.LogInformation("Checking status"); 28 | return "ok"; 29 | } 30 | 31 | [HttpGet("variants/{toggleName}")] 32 | public IEnumerable GetVariants([FromRoute] string toggleName) 33 | { 34 | _logger.LogInformation("Getting variants from toggle {0}", toggleName); 35 | return _unleash.GetVariants(toggleName); 36 | } 37 | 38 | [HttpGet("variants/{toggleName}/weighted")] 39 | public Variant GetVariant([FromRoute] string toggleName) 40 | { 41 | _logger.LogInformation("Getting weighted variant from toggle {0}", toggleName); 42 | return _unleash.GetVariant(toggleName); 43 | } 44 | 45 | [HttpGet("variants/{toggleName}/{variantName}")] 46 | public IEnumerable GetVariantsByName([FromRoute] string toggleName, [FromRoute] string variantName) 47 | { 48 | _logger.LogInformation("Getting variants from toggle {0} and variant {1}", toggleName, variantName); 49 | return _unleash.GetVariants(toggleName).Where(v => v.Name == variantName); 50 | } 51 | 52 | [HttpGet("toggles/{toggleName}")] 53 | public bool GetToggle([FromRoute] string toggleName) 54 | { 55 | _logger.LogInformation("Getting toggle {0}", toggleName); 56 | return _unleash.IsEnabled(toggleName); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /samples/WebApplication/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.Hosting; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace WebApplication 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | CreateHostBuilder(args).Build().Run(); 18 | } 19 | 20 | public static IHostBuilder CreateHostBuilder(string[] args) => 21 | Host.CreateDefaultBuilder(args) 22 | .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); 23 | } 24 | } -------------------------------------------------------------------------------- /samples/WebApplication/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:38588", 7 | "sslPort": 44357 8 | } 9 | }, 10 | "$schema": "http://json.schemastore.org/launchsettings.json", 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "WebApplication": { 19 | "commandName": "Project", 20 | "environmentVariables": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | }, 23 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 24 | }, 25 | "Docker": { 26 | "commandName": "Docker", 27 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 28 | "environmentVariables": { 29 | "ASPNETCORE_URLS": "https://+:443;http://+:80", 30 | "ASPNETCORE_HTTPS_PORT": "44358" 31 | }, 32 | "httpPort": 38589, 33 | "useSSL": true, 34 | "sslPort": 44358 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /samples/WebApplication/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using Newtonsoft.Json; 8 | using Unleash; 9 | 10 | namespace WebApplication 11 | { 12 | public class Startup 13 | { 14 | public Startup(IConfiguration configuration) 15 | { 16 | Configuration = configuration; 17 | } 18 | 19 | public IConfiguration Configuration { get; } 20 | 21 | // This method gets called by the runtime. Use this method to add services to the container. 22 | public void ConfigureServices(IServiceCollection services) 23 | { 24 | services.AddControllers(); 25 | var unleash = new DefaultUnleash(new UnleashSettings() 26 | { 27 | UnleashApi = new Uri("http://localhost:4242/api"), 28 | AppName = "variant-sample", 29 | InstanceTag = "instance 1", 30 | SendMetricsInterval = TimeSpan.FromSeconds(10), 31 | FetchTogglesInterval = TimeSpan.FromSeconds(10), 32 | }); 33 | unleash.ConfigureEvents(evtCfg => 34 | { 35 | evtCfg.ImpressionEvent = evt => { Console.WriteLine(evt.FeatureName); }; 36 | evtCfg.ErrorEvent = evt => { Console.WriteLine(evt.ErrorType + "-" + evt.Error?.Message); }; 37 | }); 38 | services.AddSingleton(c => unleash); 39 | } 40 | 41 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 42 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 43 | { 44 | if (env.IsDevelopment()) 45 | { 46 | app.UseDeveloperExceptionPage(); 47 | } 48 | 49 | app.UseHttpsRedirection(); 50 | 51 | app.UseRouting(); 52 | 53 | app.UseAuthorization(); 54 | 55 | app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /samples/WebApplication/WebApplication.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0 5 | 678b82b7-70cd-4cb1-aa57-ed9d14866742 6 | Linux 7 | ..\.. 8 | False 9 | False 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /samples/WebApplication/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/WebApplication/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "Unleash": { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /samples/WinFormsApp/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("WinFormsApp")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("WinFormsApp")] 13 | [assembly: AssemblyCopyright("Copyright © 2017")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("7a73d92c-b746-48f6-bf60-c69382141c2d")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /samples/WinFormsApp/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace WinFormsApp.Properties 12 | { 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources 26 | { 27 | 28 | private static global::System.Resources.ResourceManager resourceMan; 29 | 30 | private static global::System.Globalization.CultureInfo resourceCulture; 31 | 32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 33 | internal Resources() 34 | { 35 | } 36 | 37 | /// 38 | /// Returns the cached ResourceManager instance used by this class. 39 | /// 40 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 41 | internal static global::System.Resources.ResourceManager ResourceManager 42 | { 43 | get 44 | { 45 | if ((resourceMan == null)) 46 | { 47 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WinFormsApp.Properties.Resources", typeof(Resources).Assembly); 48 | resourceMan = temp; 49 | } 50 | return resourceMan; 51 | } 52 | } 53 | 54 | /// 55 | /// Overrides the current thread's CurrentUICulture property for all 56 | /// resource lookups using this strongly typed resource class. 57 | /// 58 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 59 | internal static global::System.Globalization.CultureInfo Culture 60 | { 61 | get 62 | { 63 | return resourceCulture; 64 | } 65 | set 66 | { 67 | resourceCulture = value; 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /samples/WinFormsApp/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace WinFormsApp.Properties 12 | { 13 | 14 | 15 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 16 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] 17 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase 18 | { 19 | 20 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 21 | 22 | public static Settings Default 23 | { 24 | get 25 | { 26 | return defaultInstance; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /samples/WinFormsApp/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /samples/WinFormsApp/UnleashForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Windows.Forms; 5 | using Unleash; 6 | 7 | namespace WinFormsApp 8 | { 9 | public partial class UnleashForm : Form 10 | { 11 | public IUnleash Unleash { get; set; } 12 | public UnleashSettings Settings { get; set; } 13 | 14 | public UnleashForm() 15 | { 16 | InitializeComponent(); 17 | } 18 | 19 | private void timer1_Tick(object sender, EventArgs e) 20 | { 21 | UpdateLabel(); 22 | } 23 | 24 | private void UpdateLabel() 25 | { 26 | var enabled = Unleash.IsEnabled(ToggleNameTextBox.Text); 27 | 28 | var enabledString = enabled 29 | ? "enabled" 30 | : "disabled"; 31 | 32 | ResultLabel.Text = $"{ToggleNameTextBox.Text} is {enabledString} for user {UsernameTextBox.Text}"; 33 | } 34 | 35 | private void linkLabel1_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) 36 | { 37 | Process.Start($"http://unleash.herokuapp.com/#/features/view/{ToggleNameTextBox.Text}"); 38 | } 39 | 40 | private void button1_Click(object sender, EventArgs e) 41 | { 42 | File.Delete(Settings.GetFeatureToggleFilePath()); 43 | File.Delete(Settings.GetFeatureToggleETagFilePath()); 44 | } 45 | 46 | private void ViewJson_Click(object sender, EventArgs e) 47 | { 48 | Process.Start(Settings.GetFeatureToggleFilePath()); 49 | } 50 | 51 | private void ViewEtag_Click(object sender, EventArgs e) 52 | { 53 | Process.Start(Settings.GetFeatureToggleETagFilePath()); 54 | } 55 | 56 | private void timer2_Tick(object sender, EventArgs e) 57 | { 58 | var enabled = Unleash.IsEnabled(ToggleNameTextBox.Text); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /samples/WinFormsApp/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Unleash/ActivationStrategy.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Unleash 4 | { 5 | internal class ActivationStrategy 6 | { 7 | public ActivationStrategy(string name, Dictionary parameters) 8 | { 9 | Name = name; 10 | Parameters = parameters; 11 | } 12 | 13 | public string Name { get; } 14 | public Dictionary Parameters { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Unleash/ClientFactory/IUnleashClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Unleash.Strategies; 3 | 4 | namespace Unleash.ClientFactory 5 | { 6 | public interface IUnleashClientFactory 7 | { 8 | IUnleash CreateClient(UnleashSettings settings, bool synchronousInitialization = false, params IStrategy[] strategies); 9 | Task CreateClientAsync(UnleashSettings settings, bool synchronousInitialization = false, params IStrategy[] strategies); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Unleash/ClientFactory/UnleashClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Unleash.Strategies; 5 | 6 | namespace Unleash.ClientFactory 7 | { 8 | /// 9 | public class UnleashClientFactory : IUnleashClientFactory 10 | { 11 | private static readonly TaskFactory TaskFactory = 12 | new TaskFactory(CancellationToken.None, 13 | TaskCreationOptions.None, 14 | TaskContinuationOptions.None, 15 | TaskScheduler.Default); 16 | 17 | /// 18 | /// Initializes a new instance of Unleash client. 19 | /// 20 | /// If true, fetch and cache toggles before returning. If false, allow the unleash client schedule an initial poll of features in the background 21 | /// Custom strategies, added in addtion to builtIn strategies. 22 | public IUnleash CreateClient(UnleashSettings settings, bool synchronousInitialization = false, params IStrategy[] strategies) 23 | { 24 | if (synchronousInitialization) 25 | { 26 | settings.ScheduleFeatureToggleFetchImmediatly = false; 27 | settings.ThrowOnInitialFetchFail = true; 28 | var unleash = new DefaultUnleash(settings, strategies); 29 | try 30 | { 31 | TaskFactory 32 | .StartNew(() => unleash.services.FetchFeatureTogglesTask.ExecuteAsync(CancellationToken.None)) 33 | .Unwrap() 34 | .GetAwaiter() 35 | .GetResult(); 36 | } 37 | catch (Exception ex) 38 | { 39 | unleash.Dispose(); 40 | throw; 41 | } 42 | 43 | return unleash; 44 | } 45 | return new DefaultUnleash(settings, strategies); 46 | } 47 | 48 | 49 | /// 50 | /// Initializes a new instance of Unleash client. 51 | /// 52 | /// If true, fetch and cache toggles before returning. If false, allow the unleash client schedule an initial poll of features in the background 53 | /// Custom strategies, added in addtion to builtIn strategies. 54 | public async Task CreateClientAsync(UnleashSettings settings, bool synchronousInitialization = false, params IStrategy[] strategies) 55 | { 56 | if (synchronousInitialization) 57 | { 58 | settings.ScheduleFeatureToggleFetchImmediatly = false; 59 | settings.ThrowOnInitialFetchFail = true; 60 | var unleash = new DefaultUnleash(settings, strategies); 61 | try 62 | { 63 | await unleash.services.FetchFeatureTogglesTask.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); 64 | } 65 | catch (Exception ex) 66 | { 67 | unleash.Dispose(); 68 | throw; 69 | } 70 | return unleash; 71 | } 72 | return new DefaultUnleash(settings, strategies); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Unleash/Communication/FetchTogglesResult.cs: -------------------------------------------------------------------------------- 1 | namespace Unleash.Communication 2 | { 3 | internal class FetchTogglesResult 4 | { 5 | public bool HasChanged { get; set; } 6 | public string Etag { get; set; } 7 | public string State { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Unleash/Communication/IUnleashApiClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using LaunchDarkly.EventSource; 5 | using Unleash.Metrics; 6 | using Unleash.Streaming; 7 | 8 | namespace Unleash.Communication 9 | { 10 | internal interface IUnleashApiClient 11 | { 12 | Task FetchToggles(string etag, CancellationToken cancellationToken, bool throwOnFail = false); 13 | Task RegisterClient(ClientRegistration registration, CancellationToken cancellationToken); 14 | // TODO: Can be simplified to `using Yggdrasil;` once MetricsBucket is dropped from Unleash.Metrics 15 | Task SendMetrics(Yggdrasil.MetricsBucket metrics, CancellationToken cancellationToken); 16 | 17 | Task StartStreamingAsync(Uri apiUri, StreamingFeatureFetcher streamingEventHandler); 18 | 19 | void StopStreaming(); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Unleash/Communication/UnleashApiClientRequestHeaders.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Unleash.Communication 5 | { 6 | internal class UnleashApiClientRequestHeaders 7 | { 8 | public string AppName { get; set; } 9 | public string InstanceTag { get; set; } 10 | public string ConnectionId { get; internal set; } 11 | public string SdkVersion { get; set; } 12 | public Dictionary CustomHttpHeaders { get; set; } 13 | public IUnleashCustomHttpHeaderProvider CustomHttpHeaderProvider { get; set; } 14 | public string SupportedSpecVersion { get; internal set; } 15 | public TimeSpan SendMetricsInterval { get; set; } 16 | public TimeSpan FetchTogglesInterval { get; set; } 17 | 18 | } 19 | } -------------------------------------------------------------------------------- /src/Unleash/DefaultCustomHttpHeaderProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Unleash 5 | { 6 | internal class DefaultCustomHttpHeaderProvider : IUnleashCustomHttpHeaderProvider 7 | { 8 | public Dictionary CustomHeaders 9 | { 10 | get 11 | { 12 | return new Dictionary(); 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Unleash/DefaultHttpClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | 8 | namespace Unleash 9 | { 10 | /// 11 | public class DefaultHttpClientFactory : IHttpClientFactory 12 | { 13 | /// 14 | /// Default: 5 seconds 15 | /// 16 | public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(5); 17 | 18 | /// 19 | /// Default: 60 seconds 20 | /// 21 | public TimeSpan ServicePointConnectionLeaseTimeout { get; set; } = TimeSpan.FromSeconds(60); 22 | 23 | /// 24 | /// Default: null 25 | /// 26 | public Dictionary CustomDefaultHttpHeaders { get; set; } 27 | 28 | /// 29 | /// Default: empty dictionary 30 | /// 31 | private static readonly ConcurrentDictionary _httpClientCache = new ConcurrentDictionary(); 32 | 33 | public HttpClient Create(Uri unleashApiUri) 34 | { 35 | var key = $"{unleashApiUri.Scheme}://{unleashApiUri.DnsSafeHost}:{unleashApiUri.Port}"; 36 | 37 | return _httpClientCache.GetOrAdd(key, k => 38 | { 39 | var client = CreateHttpClientInstance(unleashApiUri); 40 | // Refresh DNS cache each 60 seconds 41 | var servicePoint = ServicePointManager.FindServicePoint(unleashApiUri); 42 | ConfigureServicePoint(servicePoint); 43 | ConfigureHttpClient(client); 44 | ConfigureDefaultRequestHeaders(client.DefaultRequestHeaders); 45 | 46 | return client; 47 | }); 48 | } 49 | 50 | protected virtual HttpClient CreateHttpClientInstance(Uri unleashApiUri) 51 | { 52 | var client = new HttpClient 53 | { 54 | BaseAddress = unleashApiUri, 55 | Timeout = Timeout 56 | }; 57 | 58 | return client; 59 | } 60 | 61 | protected virtual void ConfigureHttpClient(HttpClient httpClient) 62 | { 63 | } 64 | 65 | protected virtual void ConfigureServicePoint(ServicePoint servicePoint) 66 | { 67 | servicePoint.ConnectionLeaseTimeout = (int)ServicePointConnectionLeaseTimeout.TotalMilliseconds; 68 | } 69 | 70 | protected virtual void ConfigureDefaultRequestHeaders(HttpRequestHeaders headers) 71 | { 72 | headers.Clear(); 73 | headers.ConnectionClose = false; 74 | headers.TryAddWithoutValidation("Accept", "application/json"); 75 | headers.TryAddWithoutValidation("Content-Type", "application/json"); 76 | 77 | if (CustomDefaultHttpHeaders != null) 78 | { 79 | foreach (var httpHeader in CustomDefaultHttpHeaders) 80 | { 81 | headers.TryAddWithoutValidation(httpHeader.Key, httpHeader.Value); 82 | } 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/Unleash/DefaultUnleashContextProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Unleash 4 | { 5 | internal class DefaultUnleashContextProvider : IUnleashContextProvider 6 | { 7 | public DefaultUnleashContextProvider(UnleashContext context = null) 8 | { 9 | Context = context ?? new UnleashContext 10 | { 11 | Properties = new Dictionary(0), 12 | }; 13 | } 14 | 15 | public UnleashContext Context { get; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Unleash/Events/ErrorEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Text; 5 | 6 | namespace Unleash.Events 7 | { 8 | public class ErrorEvent 9 | { 10 | public ErrorType ErrorType { get; set; } 11 | public Exception Error { get; set; } 12 | public HttpStatusCode? StatusCode { get; internal set; } 13 | public string Resource { get; internal set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Unleash/Events/ErrorType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Unleash.Events 6 | { 7 | public enum ErrorType 8 | { 9 | Client, 10 | TogglesBackup, 11 | TogglesUpdate, 12 | Bootstrap, 13 | ImpressionEvent, 14 | FileCache 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Unleash/IHttpClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | 4 | namespace Unleash 5 | { 6 | /// 7 | /// Factory for creating HttpClient used to communicate with Unleash Server api. 8 | /// 9 | public interface IHttpClientFactory 10 | { 11 | /// 12 | /// Called a single time during application initialization. 13 | /// 14 | HttpClient Create(Uri unleashApiUri); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Unleash/IUnleashContextProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Unleash 2 | { 3 | public interface IUnleashContextProvider 4 | { 5 | UnleashContext Context { get; } 6 | } 7 | } -------------------------------------------------------------------------------- /src/Unleash/IUnleashCustomHttpHeaderProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Unleash 4 | { 5 | public interface IUnleashCustomHttpHeaderProvider 6 | { 7 | Dictionary CustomHeaders { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Unleash/Internal/EventCallbackConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Unleash.Events; 3 | 4 | namespace Unleash.Internal 5 | { 6 | public class EventCallbackConfig 7 | { 8 | public Action ImpressionEvent { get; set; } 9 | public Action ErrorEvent { get; set; } 10 | public Action TogglesUpdatedEvent { get; set; } 11 | 12 | public void RaiseError(ErrorEvent evt) 13 | { 14 | if (ErrorEvent != null) 15 | { 16 | ErrorEvent(evt); 17 | } 18 | } 19 | 20 | public void RaiseTogglesUpdated(TogglesUpdatedEvent evt) 21 | { 22 | if (TogglesUpdatedEvent != null) 23 | { 24 | TogglesUpdatedEvent(evt); 25 | } 26 | } 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Unleash/Internal/FileSystem.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | 4 | namespace Unleash.Internal 5 | { 6 | internal class FileSystem : IFileSystem 7 | { 8 | public Encoding Encoding => encoding; 9 | private readonly Encoding encoding; 10 | 11 | public FileSystem(Encoding encoding) 12 | { 13 | this.encoding = encoding; 14 | } 15 | 16 | public bool FileExists(string path) 17 | { 18 | return File.Exists(path); 19 | } 20 | 21 | public Stream FileOpenRead(string path) 22 | { 23 | return File.OpenRead(path); 24 | } 25 | 26 | public Stream FileOpenCreate(string path) 27 | { 28 | return File.Open(path, FileMode.Create); 29 | } 30 | 31 | public void WriteAllText(string path, string content) 32 | { 33 | File.WriteAllText(path, content, encoding); 34 | } 35 | 36 | public string ReadAllText(string path) 37 | { 38 | return File.ReadAllText(path, encoding); 39 | } 40 | 41 | public void Move(string sourcePath, string destPath) 42 | { 43 | File.Move(sourcePath, destPath); 44 | } 45 | 46 | public void Replace(string sourceFileName, string destinationFileName, string destinationBackupFileName) 47 | { 48 | File.Replace(sourceFileName, destinationFileName, destinationBackupFileName); 49 | } 50 | 51 | public void Delete(string path) 52 | { 53 | File.Delete(path); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/Unleash/Internal/IFileSystem.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | 4 | namespace Unleash.Internal 5 | { 6 | internal interface IFileSystem 7 | { 8 | Encoding Encoding { get; } 9 | bool FileExists(string path); 10 | Stream FileOpenRead(string path); 11 | Stream FileOpenCreate(string path); 12 | void WriteAllText(string path, string content); 13 | string ReadAllText(string path); 14 | void Move(string sourcePath, string destPath); 15 | void Replace(string sourceFileName, string destinationFileName, string destinationBackupFileName); 16 | void Delete(string path); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Unleash/Internal/IToggleBootstrapProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Unleash.Internal 2 | { 3 | public interface IToggleBootstrapProvider 4 | { 5 | string Read(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Unleash/Internal/ImpressionEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Unleash.Internal 6 | { 7 | public class ImpressionEvent 8 | { 9 | public string Type { get; set; } 10 | public string EventId { get; set; } 11 | public UnleashContext Context { get; set; } 12 | public bool Enabled { get; set; } 13 | public string FeatureName { get; set; } 14 | public string Variant { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Unleash/Internal/ToggleDefinition.cs: -------------------------------------------------------------------------------- 1 | using Yggdrasil; 2 | 3 | public class ToggleDefinition 4 | { 5 | public ToggleDefinition(string name, string project, string type) 6 | { 7 | Name = name; 8 | Project = project; 9 | Type = type; 10 | } 11 | public string Name { get; private set; } 12 | 13 | public string Project { get; private set; } 14 | 15 | public string Type { get; private set; } 16 | 17 | internal static ToggleDefinition FromYggdrasilDef(FeatureDefinition definition) 18 | { 19 | return new ToggleDefinition(definition.Name, definition.Project, definition.Type); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Unleash/Internal/TogglesUpdatedEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Unleash.Internal 6 | { 7 | public class TogglesUpdatedEvent 8 | { 9 | public DateTime UpdatedOn { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Unleash/Internal/UnleashSettingsValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Unleash.Internal 2 | { 3 | internal class UnleashSettingsValidator 4 | { 5 | public void Validate(UnleashSettings settings) 6 | { 7 | if (settings.UnleashApi == null) 8 | throw new UnleashException("You are required to specify an uri to an unleash service"); 9 | 10 | if (settings.AppName == null) 11 | throw new UnleashException("You are required to specify an appName"); 12 | 13 | if (settings.InstanceTag == null) 14 | throw new UnleashException("You are required to specify an instance id"); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Unleash/Internal/Variant.cs: -------------------------------------------------------------------------------- 1 | namespace Unleash.Internal 2 | { 3 | public class Variant : Yggdrasil.Variant 4 | { 5 | public static new readonly Variant DISABLED_VARIANT = UpgradeVariant(Yggdrasil.Variant.DISABLED_VARIANT); 6 | 7 | public Variant(string name, Yggdrasil.Payload payload, bool enabled, bool feature_enabled) 8 | : base(name, payload, enabled, feature_enabled) 9 | { 10 | } 11 | 12 | internal static Variant UpgradeVariant(Yggdrasil.Variant variant) 13 | { 14 | return new Variant(variant.Name, variant.Payload, variant.Enabled, variant.FeatureEnabled); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Unleash/Metrics/ClientMetrics.cs: -------------------------------------------------------------------------------- 1 | namespace Unleash.Metrics 2 | { 3 | internal class ClientMetrics 4 | { 5 | public string AppName { get; set; } 6 | public string InstanceId { get; set; } 7 | public string ConnectionId { get; set; } 8 | public Yggdrasil.MetricsBucket Bucket { get; set; } 9 | public string PlatformName 10 | { 11 | get 12 | { 13 | return MetricsMetadata.GetPlatformName(); 14 | 15 | } 16 | } 17 | public string PlatformVersion 18 | { 19 | get 20 | { 21 | return MetricsMetadata.GetPlatformVersion(); 22 | } 23 | } 24 | public string YggdrasilVersion 25 | { 26 | get 27 | { 28 | return "0.14.0"; 29 | } 30 | } 31 | public string SpecVersion 32 | { 33 | get 34 | { 35 | return UnleashServices.supportedSpecVersion; 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Unleash/Metrics/ClientRegistration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Unleash.Metrics 5 | { 6 | internal class ClientRegistration 7 | { 8 | public string AppName { get; set; } 9 | public string InstanceId { get; set; } 10 | public string ConnectionId { get; set; } 11 | public string SdkVersion { get; set; } 12 | public List Strategies { get; set; } 13 | public DateTimeOffset Started { get; set; } 14 | public long Interval { get; set; } 15 | public string PlatformName 16 | { 17 | get 18 | { 19 | return MetricsMetadata.GetPlatformName(); 20 | 21 | } 22 | } 23 | public string PlatformVersion 24 | { 25 | get 26 | { 27 | return MetricsMetadata.GetPlatformVersion(); 28 | } 29 | } 30 | public string YggdrasilVersion 31 | { 32 | get 33 | { 34 | return "0.14.0"; 35 | } 36 | } 37 | public string SpecVersion 38 | { 39 | get 40 | { 41 | return UnleashServices.supportedSpecVersion; 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/Unleash/Metrics/MetricsMetadata.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | internal static class MetricsMetadata 4 | { 5 | internal static string GetPlatformName() 6 | { 7 | #if NETSTANDARD1_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER 8 | return GetPlatformName(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription); 9 | #else 10 | return "PreDotNetCore"; 11 | #endif 12 | } 13 | 14 | internal static string GetPlatformName(string frameworkDescription) 15 | { 16 | var match = Regex.Match(frameworkDescription, @"^(?.+?) \d"); 17 | return match.Success ? match.Groups["name"].Value : frameworkDescription; 18 | } 19 | 20 | internal static string GetPlatformVersion() 21 | { 22 | #if NETSTANDARD1_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER 23 | return GetPlatformVersion(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription); 24 | #else 25 | return null; 26 | #endif 27 | } 28 | 29 | internal static string GetPlatformVersion(string frameworkDescription) 30 | { 31 | var match = Regex.Match(frameworkDescription, @"(\d+\.\d+\.\d+)"); 32 | return match.Success ? match.Value : null; 33 | } 34 | } -------------------------------------------------------------------------------- /src/Unleash/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Unleash.Tests")] 4 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] -------------------------------------------------------------------------------- /src/Unleash/Scheduling/ClientMetricsBackgroundTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Unleash.Communication; 5 | using Unleash.Logging; 6 | using Yggdrasil; 7 | 8 | namespace Unleash.Scheduling 9 | { 10 | internal class ClientMetricsBackgroundTask : IUnleashScheduledTask 11 | { 12 | private static readonly ILog Logger = LogProvider.GetLogger(typeof(ClientMetricsBackgroundTask)); 13 | private readonly YggdrasilEngine engine; 14 | private readonly IUnleashApiClient apiClient; 15 | private readonly UnleashSettings settings; 16 | 17 | public ClientMetricsBackgroundTask( 18 | YggdrasilEngine engine, 19 | IUnleashApiClient apiClient, 20 | UnleashSettings settings) 21 | { 22 | this.engine = engine; 23 | this.apiClient = apiClient; 24 | this.settings = settings; 25 | } 26 | 27 | public async Task ExecuteAsync(CancellationToken cancellationToken) 28 | { 29 | if (settings.SendMetricsInterval == null) 30 | return; 31 | 32 | var result = await apiClient.SendMetrics(engine.GetMetrics(), cancellationToken).ConfigureAwait(false); 33 | 34 | // Ignore return value 35 | if (!result) 36 | { 37 | // Logged elsewhere. 38 | } 39 | } 40 | 41 | public string Name => "report-metrics-task"; 42 | public TimeSpan Interval { get; set; } 43 | public bool ExecuteDuringStartup { get; set; } 44 | } 45 | } -------------------------------------------------------------------------------- /src/Unleash/Scheduling/ClientRegistrationBackgroundTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Unleash.Communication; 6 | using Unleash.Logging; 7 | using Unleash.Metrics; 8 | 9 | namespace Unleash.Scheduling 10 | { 11 | internal class ClientRegistrationBackgroundTask : IUnleashScheduledTask 12 | { 13 | private static readonly ILog Logger = LogProvider.GetLogger(typeof(ClientRegistrationBackgroundTask)); 14 | 15 | private readonly IUnleashApiClient apiClient; 16 | private readonly UnleashSettings settings; 17 | private readonly List strategies; 18 | 19 | public ClientRegistrationBackgroundTask( 20 | IUnleashApiClient apiClient, 21 | UnleashSettings settings, 22 | List strategies) 23 | { 24 | this.apiClient = apiClient; 25 | this.settings = settings; 26 | this.strategies = strategies; 27 | } 28 | 29 | public async Task ExecuteAsync(CancellationToken cancellationToken) 30 | { 31 | if (settings.SendMetricsInterval == null) 32 | return; 33 | 34 | var clientRegistration = new ClientRegistration 35 | { 36 | AppName = settings.AppName, 37 | InstanceId = settings.InstanceTag, 38 | Interval = (long)settings.SendMetricsInterval.Value.TotalMilliseconds, 39 | SdkVersion = settings.SdkVersion, 40 | Started = DateTimeOffset.UtcNow, 41 | Strategies = strategies 42 | }; 43 | 44 | var result = await apiClient.RegisterClient(clientRegistration, cancellationToken).ConfigureAwait(false); 45 | if (!result) 46 | { 47 | // Already logged.. 48 | } 49 | } 50 | 51 | public string Name => "register-client-task"; 52 | 53 | public TimeSpan Interval { get; set; } 54 | public bool ExecuteDuringStartup { get; set; } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Unleash/Scheduling/IUnleashScheduledTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Unleash.Scheduling 6 | { 7 | /// 8 | /// A scheduled task that executes in the background at given intervals. 9 | /// 10 | public interface IUnleashScheduledTask 11 | { 12 | /// 13 | /// Executes the task 14 | /// 15 | /// Cancellation token passed into the task. 16 | /// 17 | Task ExecuteAsync(CancellationToken cancellationToken); 18 | 19 | /// 20 | /// Gets the name of the task 21 | /// 22 | string Name { get; } 23 | 24 | /// 25 | /// Gets the interval of which the task should be executed. 26 | /// 27 | TimeSpan Interval { get; } 28 | 29 | /// 30 | /// Gets a flag indicating that the task should run during startup. 31 | /// 32 | bool ExecuteDuringStartup { get; } 33 | } 34 | } -------------------------------------------------------------------------------- /src/Unleash/Scheduling/IUnleashScheduledTaskManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | 5 | namespace Unleash.Scheduling 6 | { 7 | /// 8 | /// 9 | /// Task manager for scheduling tasks on a background thread. 10 | /// 11 | public interface IUnleashScheduledTaskManager : IDisposable 12 | { 13 | /// 14 | /// Configures a set of tasks to execute in the background. 15 | /// 16 | /// Tasks to be executed 17 | /// Cancellation token which will be passed during shutdown (Dispose). 18 | void Configure(IEnumerable tasks, CancellationToken cancellationToken); 19 | } 20 | } -------------------------------------------------------------------------------- /src/Unleash/Strategies/IStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Yggdrasil; 3 | 4 | 5 | namespace Unleash.Strategies 6 | { 7 | using System.Collections.Generic; 8 | 9 | /// 10 | /// Defines a strategy for enabling a feature. 11 | /// 12 | public interface IStrategy 13 | { 14 | /// 15 | /// Gets the strategy name 16 | /// 17 | string Name { get; } 18 | 19 | /// 20 | /// Calculates if the strategy is enabled for a given context 21 | /// 22 | bool IsEnabled(Dictionary parameters, UnleashContext context); 23 | } 24 | 25 | internal class CustomStrategyAdapter : Yggdrasil.IStrategy 26 | { 27 | private IStrategy strategy { get; } 28 | 29 | public CustomStrategyAdapter(IStrategy strategy) 30 | { 31 | this.strategy = strategy; 32 | } 33 | 34 | public string Name => strategy.Name; 35 | 36 | public bool IsEnabled(Dictionary parameters, Context context) 37 | { 38 | var currentTime = context.CurrentTime ?? DateTimeOffset.UtcNow; 39 | 40 | var unleashContext = new UnleashContext.Builder() 41 | .AppName(context.AppName) 42 | .CurrentTime(currentTime) 43 | .Environment(context.Environment) 44 | .UserId(context.UserId) 45 | .SessionId(context.SessionId) 46 | .RemoteAddress(context.RemoteAddress) 47 | .Build(); 48 | unleashContext.Properties = context.Properties; 49 | 50 | return strategy.IsEnabled(parameters, unleashContext); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/Unleash/UnleashException.cs: -------------------------------------------------------------------------------- 1 | namespace Unleash 2 | { 3 | using System; 4 | using System.Runtime.Serialization; 5 | 6 | /// 7 | [Serializable] 8 | public class UnleashException : Exception 9 | { 10 | /// 11 | public UnleashException() 12 | { 13 | } 14 | 15 | /// 16 | public UnleashException(string message) : base(message) 17 | { 18 | } 19 | 20 | /// 21 | public UnleashException(string message, Exception inner) : base(message, inner) 22 | { 23 | } 24 | 25 | /// 26 | protected UnleashException( 27 | SerializationInfo info, 28 | StreamingContext context) : base(info, context) 29 | { 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Unleash/Utilities/FetchingToggleBootstrapUrlFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Text; 5 | 6 | namespace Unleash.Utilities 7 | { 8 | public class FetchingToggleBootstrapUrlFailedException : UnleashException 9 | { 10 | public FetchingToggleBootstrapUrlFailedException(string errorMessage, HttpStatusCode statusCode) : base(errorMessage) 11 | { 12 | StatusCode = statusCode; 13 | } 14 | 15 | public FetchingToggleBootstrapUrlFailedException(HttpStatusCode statusCode) 16 | { 17 | StatusCode = statusCode; 18 | } 19 | 20 | public HttpStatusCode StatusCode { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Unleash/Utilities/ToggleBootstrapFileProvider.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Unleash.Internal; 3 | 4 | namespace Unleash.Utilities 5 | { 6 | public class ToggleBootstrapFileProvider : IToggleBootstrapProvider 7 | { 8 | private readonly string filePath; 9 | private readonly UnleashSettings settings; 10 | 11 | internal ToggleBootstrapFileProvider(string filePath, UnleashSettings settings) 12 | { 13 | this.filePath = filePath; 14 | this.settings = settings; 15 | } 16 | 17 | public string Read() 18 | { 19 | try 20 | { 21 | return settings.FileSystem.ReadAllText(filePath); 22 | } 23 | catch (FileNotFoundException) 24 | { 25 | return string.Empty; 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Unleash/Utilities/ToggleBootstrapUrlProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Unleash.Internal; 7 | using Unleash.Logging; 8 | 9 | namespace Unleash.Utilities 10 | { 11 | public class ToggleBootstrapUrlProvider : IToggleBootstrapProvider 12 | { 13 | private static readonly ILog Logger = LogProvider.GetLogger(typeof(ToggleBootstrapUrlProvider)); 14 | 15 | private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); 16 | private readonly HttpClient client; 17 | private readonly UnleashSettings settings; 18 | private readonly string path; 19 | private readonly bool throwOnFail; 20 | private readonly Dictionary customHeaders; 21 | 22 | public ToggleBootstrapUrlProvider(string path, HttpClient client, UnleashSettings settings, bool throwOnFail = false, Dictionary customHeaders = null) 23 | { 24 | this.path = path; 25 | this.client = client; 26 | this.settings = settings; 27 | this.throwOnFail = throwOnFail; 28 | this.customHeaders = customHeaders; 29 | } 30 | 31 | public string Read() 32 | { 33 | return Task.Run(() => FetchFile()).GetAwaiter().GetResult(); 34 | } 35 | 36 | private async Task FetchFile() 37 | { 38 | using (var request = new HttpRequestMessage(HttpMethod.Get, path)) 39 | { 40 | if (customHeaders != null) 41 | { 42 | foreach (var keyValuePair in customHeaders) 43 | { 44 | request.Headers.Add(keyValuePair.Key, keyValuePair.Value); 45 | } 46 | } 47 | 48 | using (var response = await client.SendAsync(request, cancellationTokenSource.Token).ConfigureAwait(false)) 49 | { 50 | if (!response.IsSuccessStatusCode) 51 | { 52 | var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 53 | Logger.Trace(() => $"UNLEASH: Error {response.StatusCode} from server in 'ToggleBootstrapUrlProvider.{nameof(FetchFile)}': " + error); 54 | 55 | if (throwOnFail) 56 | throw new FetchingToggleBootstrapUrlFailedException("Failed to fetch feature toggles", response.StatusCode); 57 | 58 | return null; 59 | } 60 | 61 | try 62 | { 63 | return await response.Content.ReadAsStringAsync().ConfigureAwait(false); 64 | } 65 | catch (Exception ex) 66 | { 67 | Logger.Trace(() => $"UNLEASH: Exception in 'ToggleBootstrapUrlProvider.{nameof(FetchFile)}' during reading and deserializing ToggleCollection from stream: " + ex.Message); 68 | 69 | if (throwOnFail) 70 | throw new UnleashException("Exception during reading and deserializing ToggleCollection from stream", ex); 71 | 72 | return null; 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Unleash/Utilities/WarnOnce.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Unleash.Logging; 4 | 5 | namespace Unleash.Utilities 6 | { 7 | internal class WarnOnce 8 | { 9 | private readonly ILog logger; 10 | private readonly HashSet seen = new HashSet(); 11 | 12 | public WarnOnce(ILog logger) 13 | { 14 | this.logger = logger; 15 | } 16 | 17 | public void Warn(string key, string message) 18 | { 19 | if (seen.Contains(key)) 20 | { 21 | return; 22 | } 23 | 24 | seen.Add(key); 25 | logger.Warn(() => message); 26 | } 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /tests/Unleash.Tests.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using BenchmarkDotNet.Running; 3 | using BenchmarkDotNet.Attributes; 4 | using Unleash.Strategies; 5 | 6 | namespace Unleash.Benchmarks 7 | { 8 | public class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | var summary = BenchmarkRunner.Run(); 13 | } 14 | } 15 | 16 | public class Strategies 17 | { 18 | private readonly IStrategy userWithId = new UserWithIdStrategy(); 19 | private readonly IStrategy gradualRollout = new GradualRolloutUserIdStrategy(); 20 | 21 | private readonly Dictionary parameters; 22 | private readonly UnleashContext unleashContext; 23 | 24 | public Strategies() 25 | { 26 | parameters = new Dictionary() 27 | { 28 | {"userIds","seth,demo,gray" }, 29 | {"percentage","50" }, 30 | {"groupId","1" }, 31 | }; 32 | 33 | unleashContext = new UnleashContext() 34 | { 35 | UserId = "demo" 36 | }; 37 | } 38 | 39 | [Benchmark] 40 | public bool UserWithId() => userWithId.IsEnabled(parameters, unleashContext); 41 | 42 | [Benchmark] 43 | public bool GradualRollout() => gradualRollout.IsEnabled(parameters, unleashContext); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Unleash.Tests.Benchmarks/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Unleash.Benchmarks")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Unleash.Benchmarks")] 13 | [assembly: AssemblyCopyright("Copyright © 2017")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("7ffc30ae-a8af-4a60-9279-d29fe668b0f6")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/App_Data/dependent-features-missing-enabled.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "features": [ 4 | { 5 | "name": "enabled-child", 6 | "description": "Depends on parent", 7 | "enabled": true, 8 | "impressionData": false, 9 | "strategies": [ 10 | { 11 | "name": "default", 12 | "parameters": {} 13 | } 14 | ], 15 | "dependencies": [ 16 | { 17 | "feature": "enabled-parent" 18 | } 19 | ] 20 | }, 21 | { 22 | "name": "enabled-parent", 23 | "description": "A parent to depend upon", 24 | "enabled": true, 25 | "impressionData": false, 26 | "strategies": [ 27 | { 28 | "name": "default", 29 | "parameters": {} 30 | } 31 | ] 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /tests/Unleash.Tests/App_Data/empty-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1 3 | } -------------------------------------------------------------------------------- /tests/Unleash.Tests/App_Data/empty.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-dotnet-sdk/8094dfe89d20e82c51f9ffb8b2570d0c931fe4b7/tests/Unleash.Tests/App_Data/empty.json -------------------------------------------------------------------------------- /tests/Unleash.Tests/App_Data/etag-12345.txt: -------------------------------------------------------------------------------- 1 | 12345 -------------------------------------------------------------------------------- /tests/Unleash.Tests/App_Data/features-v0.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": [ 3 | { 4 | "name": "featureX", 5 | "enabled": true, 6 | "strategy": "default" 7 | }, 8 | { 9 | "name": "featureY", 10 | "enabled": false, 11 | "strategy": "baz", 12 | "parameters": { 13 | "foo": "bar" 14 | } 15 | }, 16 | { 17 | "name": "featureZ", 18 | "enabled": true, 19 | "strategy": "baz", 20 | "parameters": { 21 | "foo": "rab" 22 | } 23 | } 24 | ]} -------------------------------------------------------------------------------- /tests/Unleash.Tests/App_Data/features-v1-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "features": [] 4 | } -------------------------------------------------------------------------------- /tests/Unleash.Tests/App_Data/features-v1.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "version": 1, 4 | "features": [ 5 | { 6 | "name": "featureX", 7 | "enabled": true, 8 | "strategies": [ 9 | { 10 | "name": "default" 11 | } 12 | ] 13 | }, 14 | { 15 | "name": "featureY", 16 | "enabled": false, 17 | "strategies": [ 18 | { 19 | "name": "baz", 20 | "parameters": { 21 | "foo": "bar" 22 | } 23 | } 24 | ] 25 | 26 | }, 27 | { 28 | "name": "featureZ", 29 | "enabled": true, 30 | "strategies": [ 31 | { 32 | "name": "default" 33 | }, 34 | { 35 | "name": "hola", 36 | "parameters": { 37 | "name": "val" 38 | } 39 | } 40 | ] 41 | 42 | } 43 | ]} -------------------------------------------------------------------------------- /tests/Unleash.Tests/App_Data/impressiondata-v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "features": [ 4 | { 5 | "name": "Tests only", 6 | "description": "Where name has test in it", 7 | "enabled": true, 8 | "impressionData": true, 9 | "strategies": [ 10 | { 11 | "name": "default", 12 | "parameters": {}, 13 | "segments": [ 1 ] 14 | } 15 | ] 16 | } 17 | ], 18 | "segments": [ 19 | { 20 | "id": 1, 21 | "constraints": [ 22 | { 23 | "contextName": "name", 24 | "operator": "STR_CONTAINS", 25 | "value": "test" 26 | } 27 | ] 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /tests/Unleash.Tests/App_Data/unleash-repo-v0.json: -------------------------------------------------------------------------------- 1 | {"features": [ 2 | { 3 | "name": "presentFeature", 4 | "enabled": true, 5 | "strategy": "default" 6 | }, 7 | { 8 | "name": "enabledFeature", 9 | "enabled": true, 10 | "strategy": "default" 11 | }, 12 | { 13 | "name": "disabledFeature", 14 | "enabled": false, 15 | "strategy": "default" 16 | }, 17 | { 18 | "name": "featureCustomStrategy", 19 | "enabled": false, 20 | "strategy": "someCustomStrategy", 21 | "parameters": { 22 | "customParameter": "customValue" 23 | } 24 | } 25 | ]} -------------------------------------------------------------------------------- /tests/Unleash.Tests/App_Data/unleash-repo-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "features": [ 4 | { 5 | "name": "featureX", 6 | "enabled": true, 7 | "strategies": [ 8 | { 9 | "name": "default" 10 | } 11 | ] 12 | }, 13 | { 14 | "name": "featureY", 15 | "enabled": false, 16 | "strategies": [ 17 | { 18 | "name": "baz", 19 | "parameters": { 20 | "foo": "bar" 21 | } 22 | } 23 | ] 24 | 25 | }, 26 | { 27 | "name": "featureZ", 28 | "enabled": true, 29 | "strategies": [ 30 | { 31 | "name": "default" 32 | }, 33 | { 34 | "name": "hola", 35 | "parameters": { 36 | "name": "val" 37 | } 38 | } 39 | ] 40 | 41 | } 42 | ]} -------------------------------------------------------------------------------- /tests/Unleash.Tests/App_Data/unleash-repo-without-feature-field.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /tests/Unleash.Tests/BaseTest.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using NUnit.Framework; 3 | 4 | namespace Unleash.Tests 5 | { 6 | public abstract class BaseTest 7 | { 8 | protected string AppDataFile(string filename) 9 | { 10 | var file = Path.Combine(TestContext.CurrentContext.TestDirectory, "App_Data", "features-v1.json"); 11 | return file; 12 | } 13 | 14 | [SetUp] 15 | public void Setup() 16 | { 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /tests/Unleash.Tests/Communication/MockHttpClient.cs: -------------------------------------------------------------------------------- 1 | using Unleash.Communication; 2 | using Unleash.Internal; 3 | using RichardSzalay.MockHttp; 4 | 5 | namespace Unleash.Tests.Communication 6 | { 7 | internal static class MockHttpClient 8 | { 9 | internal static Tuple MakeMockClient(string url) 10 | { 11 | var mockHttp = new MockHttpMessageHandler(); 12 | 13 | var httpClient = new HttpClient(mockHttp) 14 | { 15 | BaseAddress = new Uri(url) 16 | }; 17 | 18 | var requestHeaders = new UnleashApiClientRequestHeaders 19 | { 20 | AppName = "api-test-client", 21 | ConnectionId = "00000000-0000-4000-a000-000000000000", 22 | SdkVersion = "unleash-client-mock:0.0.0", 23 | CustomHttpHeaders = new Dictionary() 24 | { 25 | { "Authorization", "*:default.some-mock-hash" } 26 | }, 27 | CustomHttpHeaderProvider = null 28 | }; 29 | 30 | var unleashClient = new UnleashApiClient(httpClient, requestHeaders, new EventCallbackConfig()); 31 | return new Tuple(mockHttp, unleashClient); 32 | 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /tests/Unleash.Tests/Communication/RequestMessageHeadersTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace Unleash.Tests.Communication 4 | { 5 | public class RequestMessageHeadersTest 6 | { 7 | [Test] 8 | public void RequestMessageHeaders_Should_Contain_Headers() 9 | { 10 | var requestMessage = new HttpRequestMessage(); 11 | requestMessage.Headers.TryAddWithoutValidation("header1", "value1"); 12 | requestMessage.Headers.TryAddWithoutValidation("header1", "value2"); 13 | 14 | Assert.That(requestMessage.Headers.Count, Is.EqualTo(1)); 15 | Assert.That(requestMessage.Headers.First().Key, Is.EqualTo("header1")); 16 | Assert.That(requestMessage.Headers.First().Value.Last(), Is.EqualTo("value2")); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /tests/Unleash.Tests/Communication/UnleasRequestBodyUnitTest.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http; 3 | using System.Text.Json; 4 | using FluentAssertions; 5 | using NUnit.Framework; 6 | using Unleash.Communication; 7 | using Unleash.Metrics; 8 | using Unleash.Tests.Mock; 9 | using Yggdrasil; 10 | 11 | namespace Unleash.Tests.Communication 12 | { 13 | public class UnleashRequestBodyUnitTest 14 | { 15 | private const string ExpectedConnectionId = "00000000-0000-4000-a000-000000000000"; 16 | 17 | private MockHttpMessageHandler _messageHandler; 18 | private IUnleashApiClient _apiClient; 19 | 20 | [SetUp] 21 | public void SetupTest() 22 | { 23 | _messageHandler = new MockHttpMessageHandler(); 24 | _apiClient = CreateApiClient(); 25 | } 26 | 27 | private IUnleashApiClient CreateApiClient() 28 | { 29 | var requestParams = new UnleashApiClientRequestHeaders 30 | { 31 | ConnectionId = ExpectedConnectionId, 32 | }; 33 | 34 | var httpClient = new HttpClient(_messageHandler) 35 | { 36 | BaseAddress = new Uri("http://example.com") 37 | }; 38 | 39 | return new UnleashApiClient(httpClient, requestParams, null); 40 | } 41 | 42 | private class MockHttpMessageHandler : HttpMessageHandler 43 | { 44 | public List Calls { get; } = new List(); 45 | public List RequestBodies { get; } = new List(); 46 | 47 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 48 | { 49 | Calls.Add(request); 50 | if (request.Content != null) 51 | { 52 | string requestBody = await request.Content.ReadAsStringAsync(); 53 | RequestBodies.Add(requestBody); 54 | } 55 | return new HttpResponseMessage(HttpStatusCode.OK); 56 | } 57 | } 58 | 59 | [Test] 60 | public async Task Should_Send_Correct_ConnectionId_In_Body_When_Sending_Metrics_And_Registering() 61 | { 62 | var engine = new YggdrasilEngine(); 63 | var metricsResult = await _apiClient.SendMetrics(engine.GetMetrics(), CancellationToken.None); 64 | var registerResult = await _apiClient.RegisterClient(new ClientRegistration(), CancellationToken.None); 65 | 66 | _messageHandler.Calls.Count.Should().Be(2); 67 | _messageHandler.RequestBodies.All(body => MatchesExpectedConnectionId(body)).Should().BeTrue(); 68 | } 69 | 70 | private bool MatchesExpectedConnectionId(string requestBody) 71 | { 72 | using (JsonDocument doc = JsonDocument.Parse(requestBody)) 73 | { 74 | return doc.RootElement.TryGetProperty("connectionId", out JsonElement connectionIdElement) && 75 | connectionIdElement.ValueKind == JsonValueKind.String && 76 | connectionIdElement.GetString() == ExpectedConnectionId; 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/Communication/UnleashApiClient_Features_Tests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using NUnit.Framework; 3 | using Unleash.Communication; 4 | using Unleash.Internal; 5 | using Unleash.Tests.Mock; 6 | 7 | namespace Unleash.Tests.Communication 8 | { 9 | public class UnleashApiClient_Features_Tests 10 | { 11 | private UnleashApiClient NewTestableClient(MockHttpMessageHandler messageHandler) 12 | { 13 | var apiUri = new Uri("http://unleash.herokuapp.com/api/"); 14 | 15 | var requestHeaders = new UnleashApiClientRequestHeaders 16 | { 17 | AppName = "api-test-client", 18 | InstanceTag = "instance1", 19 | ConnectionId = "00000000-0000-4000-a000-000000000000", 20 | SdkVersion = "unleash-client-mock:0.0.0", 21 | CustomHttpHeaders = null, 22 | CustomHttpHeaderProvider = null 23 | }; 24 | 25 | var httpClient = new HttpClient(messageHandler) 26 | { 27 | BaseAddress = apiUri, 28 | Timeout = TimeSpan.FromSeconds(5) 29 | }; 30 | 31 | return new UnleashApiClient(httpClient, requestHeaders, new EventCallbackConfig()); 32 | } 33 | 34 | [Test] 35 | public async Task WeakEtagIsCorrectlyAddedToRequests() 36 | { 37 | var weakETag = "W/\"1d7-RkvEPkkxAVI06R3w4JXj3w==\""; 38 | var messageHandler = new MockHttpMessageHandler(); 39 | messageHandler.ETagToReturn = weakETag; 40 | var client = NewTestableClient(messageHandler); 41 | 42 | var toggles = await client.FetchToggles(weakETag, CancellationToken.None); 43 | toggles.HasChanged.Should().BeFalse(); 44 | } 45 | 46 | [Test] 47 | public async Task StrongEtagIsCorrectlyAddedToRequests() 48 | { 49 | var strongETag = "\"1d7-RkvEPkkxAVI06R3w4JXj3w==\""; 50 | var messageHandler = new MockHttpMessageHandler(); 51 | messageHandler.ETagToReturn = strongETag; 52 | var client = NewTestableClient(messageHandler); 53 | 54 | var toggles = await client.FetchToggles(strongETag, CancellationToken.None); 55 | toggles.HasChanged.Should().BeFalse(); 56 | } 57 | 58 | [Test] 59 | public async Task NoEtagReturnsEmptyToggles() 60 | { 61 | var messageHandler = new MockHttpMessageHandler(); 62 | var client = NewTestableClient(messageHandler); 63 | var toggles = await client.FetchToggles(null, CancellationToken.None); 64 | //cheating here because the SDK considers an empty response as no changes 65 | toggles.Etag.Should().BeNull(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/Communication/UnleashApiClient_Project_Tests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using NUnit.Framework; 3 | using Unleash.Communication; 4 | using Unleash.Internal; 5 | using Unleash.Tests.Mock; 6 | 7 | namespace Unleash.Tests.Communication 8 | { 9 | public class UnleashApiClient_Project_Tests 10 | { 11 | private UnleashApiClient NewTestableClient(string project, MockHttpMessageHandler messageHandler) 12 | { 13 | var apiUri = new Uri("http://unleash.herokuapp.com/api/"); 14 | 15 | var requestHeaders = new UnleashApiClientRequestHeaders 16 | { 17 | AppName = "api-test-client", 18 | InstanceTag = "instance1", 19 | ConnectionId = "00000000-0000-4000-a000-000000000000", 20 | SdkVersion = "unleash-client-mock:0.0.0", 21 | CustomHttpHeaders = null, 22 | CustomHttpHeaderProvider = null 23 | }; 24 | 25 | var httpClient = new HttpClient(messageHandler) 26 | { 27 | BaseAddress = apiUri, 28 | Timeout = TimeSpan.FromSeconds(5) 29 | }; 30 | 31 | return new UnleashApiClient(httpClient, requestHeaders, new EventCallbackConfig(), project); 32 | } 33 | 34 | [Test] 35 | public async Task FetchToggles_ForProject() 36 | { 37 | var project = "testproject"; 38 | var messageHandler = new MockHttpMessageHandler(); 39 | var client = NewTestableClient(project, messageHandler); 40 | 41 | var toggles = await client.FetchToggles("", CancellationToken.None); 42 | toggles.Should().NotBeNull(); 43 | 44 | messageHandler.SentMessages.Count.Should().Be(1); 45 | messageHandler.SentMessages.First().RequestUri.Query.Should().Be("?project=" + project); 46 | } 47 | 48 | [Test] 49 | public async Task FetchToggles_WithoutProject() 50 | { 51 | string project = null; 52 | var messageHandler = new MockHttpMessageHandler(); 53 | var client = NewTestableClient(project, messageHandler); 54 | 55 | var toggles = await client.FetchToggles("", CancellationToken.None); 56 | toggles.Should().NotBeNull(); 57 | 58 | messageHandler.SentMessages.Count.Should().Be(1); 59 | messageHandler.SentMessages.First().RequestUri.Query.Should().Be(""); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/Communication/UnleashApiClient_RegisterClient_Tests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using RichardSzalay.MockHttp; 3 | using Unleash.Metrics; 4 | 5 | namespace Unleash.Tests.Communication 6 | { 7 | public class UnleashApiClient_RegisterClient_Tests 8 | { 9 | private const string BASE_URL = "http://some-mock-url/api/client"; 10 | 11 | [Test] 12 | public async Task RegisterClient_Success() 13 | { 14 | var (mockHttp, client) = MockHttpClient.MakeMockClient(BASE_URL); 15 | 16 | mockHttp.When($"{BASE_URL}/register") 17 | .WithPartialContent("\"appName\":\"SomeTestAppName\"") 18 | .WithPartialContent("\"interval\":1000") 19 | .WithPartialContent("\"sdkVersion\":\"1.0.1\"") 20 | .WithPartialContent("\"strategies\":[\"abc\"]") 21 | .WithPartialContent("specVersion") 22 | .WithPartialContent("platformName") 23 | .WithPartialContent("platformVersion") 24 | .WithPartialContent("\"yggdrasilVersion\":\"0.14.0\"") 25 | .Respond("application/json", "{ 'status': 'ok' }"); 26 | 27 | var clientRegistration = new ClientRegistration() 28 | { 29 | AppName = "SomeTestAppName", 30 | Interval = 1000, 31 | SdkVersion = "1.0.1", 32 | Strategies = new List 33 | { 34 | "abc" 35 | } 36 | }; 37 | 38 | var result = await client.RegisterClient(clientRegistration, CancellationToken.None); 39 | Assert.IsTrue(result); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /tests/Unleash.Tests/Communication/UnleashApiClient_SendMetrics_Tests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using RichardSzalay.MockHttp; 3 | using NUnit.Framework.Internal; 4 | using Yggdrasil; 5 | 6 | namespace Unleash.Tests.Communication 7 | { 8 | public class UnleashApiClient_SendMetrics_Tests 9 | { 10 | private const string BASE_URL = "http://some-mock-url/api/client"; 11 | 12 | [Test] 13 | public async Task SendMetrics_Success() 14 | { 15 | var (mockHttp, client) = MockHttpClient.MakeMockClient(BASE_URL); 16 | 17 | mockHttp.When($"{BASE_URL}/metrics") 18 | .WithPartialContent("appName") 19 | .WithPartialContent("instanceId") 20 | .WithPartialContent("\"no\":0") 21 | .WithPartialContent("\"yes\":1") 22 | .WithPartialContent("specVersion") 23 | .WithPartialContent("platformName") 24 | .WithPartialContent("platformVersion") 25 | .WithPartialContent("\"yggdrasilVersion\":\"0.14.0\"") 26 | .Respond("application/json", "{ 'status': 'ok' }"); 27 | 28 | var engine = new YggdrasilEngine(); 29 | engine.CountFeature("someTestToggle", true); 30 | 31 | var result = await client.SendMetrics(engine.GetMetrics(), CancellationToken.None); 32 | Assert.IsTrue(result); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/Communication/UnleashHttpClientFactory_RegisterHttpClient_Tests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | 4 | namespace Unleash.Tests.Communication 5 | { 6 | public class UnleashHttpClientFactory_RegisterHttpClient_Tests 7 | { 8 | private readonly Uri apiUri = new Uri("http://unleash.herokuapp.com/api/"); 9 | 10 | private readonly Uri newApiUri = new Uri("http://unleash2.herokuapp2.com/api/"); 11 | 12 | private readonly DefaultHttpClientFactory httpClientFactory = new DefaultHttpClientFactory(); 13 | 14 | [Test] 15 | public void HttpClientFactory_Should_Create_Single_Instance_By_Dns_Success() 16 | { 17 | var httpClient = httpClientFactory.Create(apiUri); 18 | var expectedHashId = httpClient.GetHashCode(); 19 | 20 | var newHttpClient = httpClientFactory.Create(apiUri); 21 | var actualHashId = newHttpClient.GetHashCode(); 22 | 23 | Assert.That(actualHashId, Is.EqualTo(expectedHashId)); 24 | } 25 | 26 | [Test] 27 | public void HttpClientFactory_Should_Create_One_Instance_Per_Dns_Success() 28 | { 29 | var httpClient = httpClientFactory.Create(apiUri); 30 | var expectedHashId = httpClient.GetHashCode(); 31 | 32 | var newHttpClient = httpClientFactory.Create(newApiUri); 33 | var actualHashId = newHttpClient.GetHashCode(); 34 | 35 | Assert.That(actualHashId, Is.Not.EqualTo(expectedHashId)); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/ExampleTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using FluentAssertions; 3 | using NUnit.Framework; 4 | using Unleash.ClientFactory; 5 | 6 | namespace Unleash.Tests 7 | { 8 | 9 | public class ExampleTests 10 | { 11 | private IUnleash unleash; 12 | 13 | [SetUp] 14 | public async Task Setup() 15 | { 16 | var factory = new UnleashClientFactory(); 17 | unleash = await factory.CreateClientAsync(new MockedUnleashSettings(instanceTag: "test instance ExampleTests"), true); 18 | } 19 | 20 | [Test] 21 | public void UserAEnabled() 22 | { 23 | unleash.IsEnabled("one-enabled") 24 | .Should().BeTrue(); 25 | } 26 | 27 | [Test] 28 | public void DisabledFeature() 29 | { 30 | unleash.IsEnabled("one-disabled") 31 | .Should().BeFalse(); 32 | } 33 | 34 | [TearDown] 35 | public void Dispose() 36 | { 37 | unleash?.Dispose(); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /tests/Unleash.Tests/Integration/Data/.gitignore: -------------------------------------------------------------------------------- 1 | *.json -------------------------------------------------------------------------------- /tests/Unleash.Tests/Integration/Data/README: -------------------------------------------------------------------------------- 1 | Run the following PowerShell command in the repository root directory to 2 | download client specifications needed for running the tests in Visual Studio: 3 | 4 | .\build.ps1 -Target Download-Client-Specifications 5 | 6 | The specifications are automatically downloaded when building with Cake. 7 | 8 | See https://github.com/Unleash/client-specification for more information 9 | regarding client specifications. -------------------------------------------------------------------------------- /tests/Unleash.Tests/Integration/TestCase.cs: -------------------------------------------------------------------------------- 1 | namespace Unleash.Tests.Specifications 2 | { 3 | public class TestCase 4 | { 5 | public string Description { get; set; } 6 | public UnleashContextDefinition Context { get; set; } 7 | public string ToggleName { get; set; } 8 | public bool ExpectedResult { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/Integration/TestCaseVariant.cs: -------------------------------------------------------------------------------- 1 | using Unleash.Internal; 2 | 3 | namespace Unleash.Tests.Specifications 4 | { 5 | public class TestCaseVariant 6 | { 7 | public string Description { get; set; } 8 | public UnleashContextDefinition Context { get; set; } 9 | public string ToggleName { get; set; } 10 | public Variant ExpectedResult { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/Integration/TestDefinition.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System.Collections.Generic; 3 | 4 | namespace Unleash.Tests.Specifications 5 | { 6 | public class TestDefinition 7 | { 8 | public string Name { get; set; } 9 | public JObject State { get; set; } 10 | public List Tests { get; set; } 11 | public List VariantTests { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/Integration/UnleashContextDefinition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Unleash.Tests.Specifications 5 | { 6 | public class UnleashContextDefinition 7 | { 8 | public string UserId { get; set; } 9 | public string SessionId { get; set; } 10 | public string RemoteAddress { get; set; } 11 | public string Environment { get; set; } 12 | public string AppName { get; set; } 13 | public DateTimeOffset? CurrentTime { get; set; } 14 | public Dictionary Properties { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/Internal/CachedFilesLoaderTestBase.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Unleash.Tests.Internal 10 | { 11 | public class CachedFilesLoaderTestBase 12 | { 13 | protected string AppDataFile(string filename) 14 | { 15 | var file = Path.Combine(TestContext.CurrentContext.TestDirectory, "App_Data", filename); 16 | return file; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/Metrics/MetricsMetadataTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using NUnit.Framework; 3 | 4 | public class MetricsMetadataTests 5 | { 6 | [TestCase(".NET Core 3.1.32", ".NET Core", "3.1.32")] 7 | [TestCase(".NET 7.0.12", ".NET", "7.0.12")] 8 | [TestCase(".NET", ".NET", null)] //apparently this can be a thing but the docs are very shady on when 9 | public void GetPlatformName_ShouldReturnDotNet(string inputString, string expectedName, string expectedVersion) 10 | { 11 | var platformName = MetricsMetadata.GetPlatformName(inputString); 12 | var platformVersion = MetricsMetadata.GetPlatformVersion(inputString); 13 | 14 | platformName.Should().Be(expectedName); 15 | platformVersion.Should().Be(expectedVersion); 16 | } 17 | } -------------------------------------------------------------------------------- /tests/Unleash.Tests/Mock/ConfigurableMessageHandlerMock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Unleash.Tests.Mock 10 | { 11 | public class ConfigurableMessageHandlerMock : HttpMessageHandler 12 | { 13 | private Dictionary configuredResponses = new Dictionary(); 14 | public List SentMessages = new List(); 15 | 16 | 17 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 18 | { 19 | SentMessages.Add(request); 20 | 21 | return Task.FromResult(configuredResponses[request.RequestUri.ToString()]); 22 | } 23 | 24 | public void Configure(string path, HttpResponseMessage response) 25 | { 26 | configuredResponses.Add(path, response); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/Mock/HttpClientFactoryMock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Unleash.Tests.Mock 9 | { 10 | internal class HttpClientFactoryMock : DefaultHttpClientFactory 11 | { 12 | public bool CreateHttpClientInstanceCalled { get; private set; } 13 | 14 | protected override HttpClient CreateHttpClientInstance(Uri unleashApiUri) 15 | { 16 | CreateHttpClientInstanceCalled = true; 17 | 18 | return base.CreateHttpClientInstance(unleashApiUri); 19 | } 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/Mock/MockApiClient.cs: -------------------------------------------------------------------------------- 1 | using Unleash.Communication; 2 | using Unleash.Metrics; 3 | using Unleash.Streaming; 4 | using Yggdrasil; 5 | 6 | namespace Unleash.Tests.Mock 7 | { 8 | internal class MockApiClient : IUnleashApiClient 9 | { 10 | private static readonly string State = @" 11 | { 12 | ""version"": 2, 13 | ""features"": [ 14 | { 15 | ""name"": ""one-enabled"", 16 | ""type"": ""release"", 17 | ""enabled"": true, 18 | ""impressionData"": false, 19 | ""strategies"": [ 20 | { 21 | ""name"": ""userWithId"", 22 | ""parameters"": { 23 | ""userIds"": ""userA"" 24 | } 25 | } 26 | ], 27 | ""variants"": [ 28 | { 29 | ""name"": ""Aa"", 30 | ""weight"": 33 31 | }, 32 | { 33 | ""name"": ""Aa"", 34 | ""weight"": 33 35 | }, 36 | { 37 | ""name"": ""Ab"", 38 | ""weight"": 34, 39 | ""overrides"": [ 40 | { 41 | ""contextName"": ""context"", 42 | ""values"": [""a"", ""b""] 43 | } 44 | ] 45 | } 46 | ] 47 | }, 48 | { 49 | ""name"": ""one-disabled"", 50 | ""type"": ""release"", 51 | ""enabled"": false, 52 | ""impression-data"": false, 53 | ""strategies"": [ 54 | { 55 | ""name"": ""userWithId"", 56 | ""parameters"": { 57 | ""userIds"": ""userB"" 58 | } 59 | } 60 | ] 61 | } 62 | ] 63 | }"; 64 | 65 | public Task FetchToggles(string etag, CancellationToken cancellationToken, bool throwOnFail = false) 66 | { 67 | return Task.Run(async delegate 68 | { 69 | await Task.Delay(200); 70 | return new FetchTogglesResult 71 | { 72 | HasChanged = true, 73 | Etag = "etag", 74 | State = State 75 | }; 76 | }); 77 | } 78 | 79 | public Task RegisterClient(ClientRegistration registration, CancellationToken cancellationToken) 80 | { 81 | return Task.FromResult(true); 82 | } 83 | 84 | public Task SendMetrics(MetricsBucket metricsBucket, CancellationToken cancellationToken) 85 | { 86 | return Task.FromResult(true); 87 | } 88 | 89 | public Task StartStreamingAsync(Uri apiUri, StreamingFeatureFetcher streamingEventHandler) 90 | { 91 | throw new NotImplementedException(); 92 | } 93 | 94 | public void StopStreaming() 95 | { 96 | throw new NotImplementedException(); 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /tests/Unleash.Tests/Mock/MockFileSystem.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Unleash.Internal; 3 | 4 | namespace Unleash.Tests.Mock 5 | { 6 | class MockFileSystem : IFileSystem 7 | { 8 | internal readonly Dictionary _fileSystem = new(); 9 | public Encoding Encoding => Encoding.UTF8; 10 | 11 | public bool FileExists(string path) 12 | { 13 | return _fileSystem.ContainsKey(path); 14 | } 15 | 16 | public Stream FileOpenRead(string path) 17 | { 18 | if (_fileSystem.TryGetValue(path, out var content)) 19 | { 20 | return new TrackingWriteStream(path, _fileSystem, Encoding.UTF8); 21 | } 22 | throw new FileNotFoundException(); 23 | } 24 | 25 | public Stream FileOpenCreate(string path) 26 | { 27 | return new TrackingWriteStream(path, _fileSystem, Encoding.UTF8); 28 | } 29 | 30 | public void WriteAllText(string path, string content) 31 | { 32 | _fileSystem[path] = content; 33 | } 34 | 35 | public string ReadAllText(string path) 36 | { 37 | if (!_fileSystem.TryGetValue(path, out var content)) 38 | { 39 | throw new FileNotFoundException(); 40 | } 41 | return content!; 42 | } 43 | 44 | public void Move(string sourcePath, string destPath) 45 | { 46 | if (_fileSystem.TryGetValue(sourcePath, out var content)) 47 | { 48 | _fileSystem.Remove(sourcePath); 49 | _fileSystem[destPath] = content; 50 | } 51 | } 52 | 53 | public void Replace(string sourceFileName, string destinationFileName, string destinationBackupFileName) 54 | { 55 | if (_fileSystem.TryGetValue(sourceFileName, out var content)) 56 | { 57 | _fileSystem.Remove(sourceFileName); 58 | _fileSystem[destinationFileName] = content; 59 | _fileSystem[destinationBackupFileName] = content; 60 | } 61 | } 62 | 63 | public void Delete(string path) 64 | { 65 | _fileSystem.Remove(path); 66 | } 67 | 68 | internal List ListFiles() 69 | { 70 | return _fileSystem.Keys.ToList(); 71 | } 72 | } 73 | 74 | class TrackingWriteStream : MemoryStream 75 | { 76 | private readonly string _path; 77 | private readonly Dictionary _fs; 78 | private readonly Encoding _encoding; 79 | 80 | public TrackingWriteStream(string path, Dictionary fs, Encoding encoding) 81 | { 82 | _path = path; 83 | _fs = fs; 84 | _encoding = encoding; 85 | } 86 | 87 | public override void Flush() 88 | { 89 | Position = 0; 90 | using (var reader = new StreamReader(this, _encoding, leaveOpen: true)) 91 | { 92 | var content = reader.ReadToEnd(); 93 | _fs[_path] = content; 94 | } 95 | 96 | base.Flush(); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/Mock/MockHttpMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Unleash.Tests.Mock 10 | { 11 | public class MockHttpMessageHandler : HttpMessageHandler 12 | { 13 | public List SentMessages { get; } = new List(); 14 | 15 | public string? ETagToReturn { get; set; } 16 | 17 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 18 | { 19 | SentMessages.Add(request); 20 | 21 | var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK) 22 | { 23 | Content = new StringContent("{}", Encoding.UTF8, "application/json") 24 | }; 25 | 26 | if (!string.IsNullOrEmpty(ETagToReturn)) 27 | { 28 | response.Headers.ETag = System.Net.Http.Headers.EntityTagHeaderValue.Parse(ETagToReturn); 29 | } 30 | 31 | return Task.FromResult(response); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /tests/Unleash.Tests/MockedUnleashSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Unleash.Internal; 4 | using Unleash.Tests.Mock; 5 | using System.Text; 6 | 7 | namespace Unleash.Tests 8 | { 9 | public class MockedUnleashSettings : UnleashSettings 10 | { 11 | public MockedUnleashSettings(bool mockFileSystem = true, string instanceTag = "test instance 1") 12 | { 13 | AppName = "test"; 14 | InstanceTag = instanceTag; 15 | UnleashApi = new Uri("http://localhost:4242/"); 16 | DisableSingletonWarning = true; 17 | 18 | UnleashApiClient = new MockApiClient(); 19 | FileSystem = new MockFileSystem(); 20 | 21 | if (!mockFileSystem) 22 | { 23 | FileSystem = new FileSystem(Encoding.UTF8); 24 | } 25 | 26 | UnleashContextProvider = new DefaultUnleashContextProvider(new UnleashContext 27 | { 28 | UserId = "userA", 29 | SessionId = "sessionId", 30 | RemoteAddress = "remoteAddress", 31 | Properties = new Dictionary() 32 | }); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /tests/Unleash.Tests/NLog.config: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: ComVisible(false)] 6 | 7 | [assembly: Guid("ff268018-34c4-45eb-937e-2e0ab25eeeda")] 8 | -------------------------------------------------------------------------------- /tests/Unleash.Tests/UnleashSettingsTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using NUnit.Framework; 3 | 4 | namespace Unleash.Tests 5 | { 6 | public class UnleashSettingsTests 7 | { 8 | [Test] 9 | public void Should_set_environment_to_default() 10 | { 11 | // Act 12 | var settings = new UnleashSettings(); 13 | 14 | // Assert 15 | settings.Environment.Should().Be("default"); 16 | } 17 | 18 | [Test] 19 | public void Should_set_sdk_name() 20 | { 21 | // Act 22 | var settings = new UnleashSettings(); 23 | 24 | // Assert 25 | settings.SdkVersion.Should().StartWith("unleash-dotnet-sdk:"); 26 | settings.SdkVersion.Should().MatchRegex(@":\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$"); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /tests/Unleash.Tests/Utilities/ToggleBootstrapFileProvider_Tests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using NUnit.Framework; 3 | using System.Text; 4 | using Unleash.Internal; 5 | using Unleash.Tests.Mock; 6 | using Unleash.Utilities; 7 | 8 | namespace Unleash.Tests.Utilities 9 | { 10 | public class ToggleBootstrapFileProvider_Tests 11 | { 12 | protected string AppDataFile(string filename) 13 | { 14 | var file = Path.Combine(TestContext.CurrentContext.TestDirectory, "App_Data", filename); 15 | return file; 16 | } 17 | 18 | [Test] 19 | public void Returns_String_Empty_When_File_Does_Not_Exist() 20 | { 21 | // Arrange 22 | string toggleFileName = "unleash-repo-v1-missing.json"; 23 | var toggleFileProvider = new ToggleBootstrapFileProvider(toggleFileName, new UnleashSettings() { FileSystem = new MockFileSystem() }); 24 | 25 | // Act 26 | var emptyResult = toggleFileProvider.Read(); 27 | 28 | // Assert 29 | emptyResult.Should().BeEmpty(); 30 | } 31 | 32 | [Test] 33 | public void Returns_File_Content_When_File_Exists() 34 | { 35 | // Arrange 36 | var fileSystem = new MockFileSystem(); 37 | string toggleFileName = "unleash-repo-v1.json"; 38 | string fileContent = "{ \"toggles\": [] }"; 39 | 40 | fileSystem.WriteAllText(toggleFileName, fileContent); 41 | var toggleFileProvider = new ToggleBootstrapFileProvider(toggleFileName, new UnleashSettings() { FileSystem = fileSystem }); 42 | 43 | // Act 44 | var result = toggleFileProvider.Read(); 45 | 46 | // Assert 47 | result.Should().Be(fileContent); 48 | } 49 | 50 | [Test] 51 | public void Returns_File_Content_When_Configured_Through_Settings_And_File_Exists() 52 | { 53 | // Arrange 54 | var settings = new UnleashSettings(); 55 | var toggleFileName = "unleash-repo-v1.json"; 56 | var fileContent = "{ \"toggles\": [] }"; 57 | var fileSystem = new MockFileSystem(); 58 | fileSystem.WriteAllText(toggleFileName, fileContent); 59 | 60 | settings.UseBootstrapFileProvider(toggleFileName); 61 | settings.FileSystem = fileSystem; 62 | 63 | // Act 64 | var result = settings.ToggleBootstrapProvider.Read(); 65 | 66 | // Assert 67 | result.Should().Be(fileContent); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /tests/Unleash.Tests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tools/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /v5_MIGRATION_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Migrating to Unleash-Client-Dotnet 5.0.0 2 | 3 | This guide highlights the key changes you should be aware of when upgrading to v5.0.0 of the Unleash client. 4 | 5 | ## Custom strategy changes 6 | 7 | Custom strategies no longer provide the option to access the constraints in their interface. The method `bool IsEnabled(Dictionary parameters, UnleashContext context, IEnumerable constraints)` no longer exists. Other than that, custom strategies remain unchanged. 8 | 9 | ## Direct access to feature toggles 10 | 11 | Direct access to the feature toggle objects through `UnleashClient.FeatureToggles` has been removed. All classes related to the internal representation of feature toggles are no longer publicly accessible in the SDK. 12 | 13 | The SDK now provides a `UnleashClient.ListKnownToggles` method, which returns a list of feature toggle names, their type, and the project they're bound to. 14 | 15 | The client also no longer provides access to listing the variants bound to a feature flag through `UnleashClient.GetVariants`. We determined that this was exposing internal abstractions that should remain within the SDK. However, if you have a strong use case for this, please open an issue. 16 | 17 | ## Bootstrapping changes 18 | 19 | Due to the changes in the previous section, bootstrapping classes are now required to return a `String` instead of a `FeatureToggleCollection`. The string should be a JSON string representing the response returned from your Unleash instance's `api/client/features` endpoint. In practice, that means if you previously had a `Read` method in your bootstrapping class like so: 20 | 21 | ``` dotnet 22 | 23 | public ToggleCollection Read() 24 | { 25 | var json = settings.FileSystem.ReadAllText(filePath); 26 | return settings.JsonSerializer.Deserialize(json); 27 | } 28 | 29 | ``` 30 | 31 | You can simplify it to: 32 | 33 | ``` dotnet 34 | 35 | public string Read() 36 | { 37 | return settings.FileSystem.ReadAllText(filePath); 38 | } 39 | 40 | ``` 41 | 42 | ## Custom serializers 43 | 44 | In v4.x and before, the SDK provided the option of mounting a custom JSON serializer. This option has been removed in v5.x; the SDK now relies on `System.Text.Json` with no option to override it. If you previously provided a custom serializer to access `System.Text.Json`, it's now safe to remove it. 45 | 46 | If you use NewtonSoft you don't have to make any changes. --------------------------------------------------------------------------------