mirror of https://github.com/dapr/dotnet-sdk.git
Add Actions Client and ASP.NET Core integration (#61)
* DRY up project settings Moves repetative project settings into Directory.Build.props so they are not duplicated so much. I'm adding 5-6 new projects when tests are included, so it makes sense to try and commonize. * Add .vscode/ to .gitignore We really have the choice to either check in a standard set of vscode's files (if that's something the team cares about) - or ignore them so that it's not noisy for people that use the repo with VS Code. The former requires more discussion and effort, so ignoring `.vscode/` for now makes the most sense. * Add project skeletons Decoder-ring for projects: Microsoft.Dapr.Client[.Test] - client and tests Microsoft.Dapr.AspNetCore[.Test] ASP.NET Core integration and tests Microsoft.Dapr.AspNetCore.IntegrationTest[.App] integration tests RoutingSample - sample using route-to-code ControllerSample - sample using controllers * Add Microsoft.Dapr.Client * Add Routing and Controller samples * Add integration tests for MVC and routing * Use ValueTask * Different paradigm for services * Fix client constructor * Add readmes for samples * Updating pipelin yaml to package aspnetcore packages
This commit is contained in:
parent
447c93c8c2
commit
0b08737cb6
|
|
@ -75,3 +75,6 @@ bld/
|
|||
*.vsp
|
||||
*.vspx
|
||||
/drop
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
|
|
|
|||
|
|
@ -112,6 +112,22 @@ stages:
|
|||
packagesToPack: 'src/Microsoft.Dapr.Actors.AspNetCore/Microsoft.Dapr.Actors.AspNetCore.csproj'
|
||||
nobuild: true
|
||||
versioningScheme: 'off'
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Package Microsoft.Dapr.Client nuget - $(Configuration)'
|
||||
inputs:
|
||||
command: 'pack'
|
||||
arguments: '--configuration $(Configuration)'
|
||||
packagesToPack: 'src/Microsoft.Dapr.Client/Microsoft.Dapr.Client.csproj'
|
||||
nobuild: true
|
||||
versioningScheme: 'off'
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Package Microsoft.Dapr.AspNetCore nuget - $(Configuration)'
|
||||
inputs:
|
||||
command: 'pack'
|
||||
arguments: '--configuration $(Configuration)'
|
||||
packagesToPack: 'src/Microsoft.Dapr.AspNetCore/Microsoft.Dapr.AspNetCore.csproj'
|
||||
nobuild: true
|
||||
versioningScheme: 'off'
|
||||
- task: EsrpCodeSigning@1
|
||||
displayName: 'ESRP CodeSign - Nuget Package'
|
||||
condition: and(eq(variables['Configuration'], 'release'), or(contains(variables['Build.SourceBranch'], 'refs/heads/master'), startsWith(variables['Build.SourceBranch'], 'refs/tags/v')))
|
||||
|
|
|
|||
64
code.sln
64
code.sln
|
|
@ -9,6 +9,28 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Dapr.Actors.Test"
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Dapr.Actors.AspNetCore", "src\Microsoft.Dapr.Actors.AspNetCore\Microsoft.Dapr.Actors.AspNetCore.csproj", "{B9C12532-0969-4DAC-A2F8-CA9208D7A901}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{27C5D71D-0721-4221-9286-B94AB07B58CF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Dapr.Client", "src\Microsoft.Dapr.Client\Microsoft.Dapr.Client.csproj", "{62E41317-ED5D-4AA4-B129-C9E56C27354C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Dapr.AspNetCore", "src\Microsoft.Dapr.AspNetCore\Microsoft.Dapr.AspNetCore.csproj", "{08D602F6-7C11-4653-B70B-B56333BF6FD2}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{B2DB41EE-45F5-447B-95E8-38E1E8B70C4E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControllerSample", "samples\ControllerSample\ControllerSample.csproj", "{2B015C86-32FB-4178-8F8A-26749B6FCE11}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoutingSample", "samples\RoutingSample\RoutingSample.csproj", "{1FBDE4EF-D8B0-4BFE-B393-3CA4FE1944A2}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{DD020B34-460F-455F-8D17-CF4A949F100B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Dapr.Client.Test", "test\Microsoft.Dapr.Client.Test\Microsoft.Dapr.Client.Test.csproj", "{383609C1-F43F-49EB-85E4-1964EE7F0F14}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Dapr.AspNetCore.Test", "test\Microsoft.Dapr.AspNetCore.Test\Microsoft.Dapr.AspNetCore.Test.csproj", "{B314AD5E-10AC-418A-B021-D4206BF37ACF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Dapr.AspNetCore.IntegrationTest", "test\Microsoft.Dapr.AspNetCore.IntegrationTest\Microsoft.Dapr.AspNetCore.IntegrationTest.csproj", "{0CD1912D-5E27-4A2A-A998-164792E0D006}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Dapr.AspNetCore.IntegrationTest.App", "test\Microsoft.Dapr.AspNetCore.IntegrationTest.App\Microsoft.Dapr.AspNetCore.IntegrationTest.App.csproj", "{342783B5-F75B-4752-A3E2-B8CB7D09C080}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -27,6 +49,38 @@ Global
|
|||
{B9C12532-0969-4DAC-A2F8-CA9208D7A901}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B9C12532-0969-4DAC-A2F8-CA9208D7A901}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B9C12532-0969-4DAC-A2F8-CA9208D7A901}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{62E41317-ED5D-4AA4-B129-C9E56C27354C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{62E41317-ED5D-4AA4-B129-C9E56C27354C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{62E41317-ED5D-4AA4-B129-C9E56C27354C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{62E41317-ED5D-4AA4-B129-C9E56C27354C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{08D602F6-7C11-4653-B70B-B56333BF6FD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{08D602F6-7C11-4653-B70B-B56333BF6FD2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{08D602F6-7C11-4653-B70B-B56333BF6FD2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{08D602F6-7C11-4653-B70B-B56333BF6FD2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2B015C86-32FB-4178-8F8A-26749B6FCE11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2B015C86-32FB-4178-8F8A-26749B6FCE11}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2B015C86-32FB-4178-8F8A-26749B6FCE11}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2B015C86-32FB-4178-8F8A-26749B6FCE11}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1FBDE4EF-D8B0-4BFE-B393-3CA4FE1944A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1FBDE4EF-D8B0-4BFE-B393-3CA4FE1944A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1FBDE4EF-D8B0-4BFE-B393-3CA4FE1944A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1FBDE4EF-D8B0-4BFE-B393-3CA4FE1944A2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{383609C1-F43F-49EB-85E4-1964EE7F0F14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{383609C1-F43F-49EB-85E4-1964EE7F0F14}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{383609C1-F43F-49EB-85E4-1964EE7F0F14}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{383609C1-F43F-49EB-85E4-1964EE7F0F14}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B314AD5E-10AC-418A-B021-D4206BF37ACF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B314AD5E-10AC-418A-B021-D4206BF37ACF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B314AD5E-10AC-418A-B021-D4206BF37ACF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B314AD5E-10AC-418A-B021-D4206BF37ACF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0CD1912D-5E27-4A2A-A998-164792E0D006}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0CD1912D-5E27-4A2A-A998-164792E0D006}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0CD1912D-5E27-4A2A-A998-164792E0D006}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0CD1912D-5E27-4A2A-A998-164792E0D006}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{342783B5-F75B-4752-A3E2-B8CB7D09C080}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{342783B5-F75B-4752-A3E2-B8CB7D09C080}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{342783B5-F75B-4752-A3E2-B8CB7D09C080}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{342783B5-F75B-4752-A3E2-B8CB7D09C080}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -34,4 +88,14 @@ Global
|
|||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{62E41317-ED5D-4AA4-B129-C9E56C27354C} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
|
||||
{08D602F6-7C11-4653-B70B-B56333BF6FD2} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
|
||||
{2B015C86-32FB-4178-8F8A-26749B6FCE11} = {B2DB41EE-45F5-447B-95E8-38E1E8B70C4E}
|
||||
{1FBDE4EF-D8B0-4BFE-B393-3CA4FE1944A2} = {B2DB41EE-45F5-447B-95E8-38E1E8B70C4E}
|
||||
{383609C1-F43F-49EB-85E4-1964EE7F0F14} = {DD020B34-460F-455F-8D17-CF4A949F100B}
|
||||
{B314AD5E-10AC-418A-B021-D4206BF37ACF} = {DD020B34-460F-455F-8D17-CF4A949F100B}
|
||||
{0CD1912D-5E27-4A2A-A998-164792E0D006} = {DD020B34-460F-455F-8D17-CF4A949F100B}
|
||||
{342783B5-F75B-4752-A3E2-B8CB7D09C080} = {DD020B34-460F-455F-8D17-CF4A949F100B}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
namespace ControllerSample
|
||||
{
|
||||
public class Account
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public decimal Balance { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Dapr.AspNetCore\Microsoft.Dapr.AspNetCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Dapr;
|
||||
|
||||
namespace ControllerSample.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
public class SampleController : ControllerBase
|
||||
{
|
||||
[HttpGet("{account}")]
|
||||
public ActionResult<Account> Get(StateEntry<Account> account)
|
||||
{
|
||||
if (account.Value is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return account.Value;
|
||||
}
|
||||
|
||||
[Topic("deposit")]
|
||||
[HttpPost("deposit")]
|
||||
public async Task<ActionResult<Account>> Deposit(Transaction transaction, [FromServices] StateClient stateClient)
|
||||
{
|
||||
var state = await stateClient.GetStateEntryAsync<Account>(transaction.Id);
|
||||
state.Value.Balance += transaction.Amount;
|
||||
await state.SaveAsync();
|
||||
return state.Value;
|
||||
}
|
||||
|
||||
[Topic("withdraw")]
|
||||
[HttpPost("withdraw")]
|
||||
public async Task<ActionResult<Account>> Withdraw(Transaction transaction, [FromServices] StateClient stateClient)
|
||||
{
|
||||
var state = await stateClient.GetStateEntryAsync<Account>(transaction.Id);
|
||||
state.Value.Balance -= transaction.Amount;
|
||||
await state.SaveAsync();
|
||||
return state.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ControllerSample
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:15298",
|
||||
"sslPort": 44311
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "api/values",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"ControllerSample": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "api/values",
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
# ASP.NET Core Controller Sample
|
||||
|
||||
This sample shows using Dapr with ASP.NET Core routing. This application is a simple and not-so-secure banking application. The application uses the Dapr state-store for its data storage.
|
||||
|
||||
It exposes the following endpoints over HTTP:
|
||||
- GET `/{account}`: Get the balance for the account specified by `id`
|
||||
- POST `/deposit`: Accepts a JSON payload to deposit money to an account
|
||||
- POST `/withdraw`: Accepts a JSON payload to withdraw money from an account
|
||||
|
||||
The application also registers for pub-sub with the `deposit` and `withdraw` topics.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
To run the sample locally run this comment in this directory:
|
||||
```sh
|
||||
actions run --app-id routing --app-port 5000 dotnet run
|
||||
```
|
||||
|
||||
The application will listen on port 5000 for HTTP.
|
||||
|
||||
### Examples
|
||||
|
||||
**Deposit Money**
|
||||
|
||||
```sh
|
||||
curl -X POST http://localhost:5000/deposit \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "id": "17", "amount": 12 }'
|
||||
```
|
||||
|
||||
```txt
|
||||
{"id":"17","balance":12}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Withdraw Money**
|
||||
|
||||
```sh
|
||||
curl -X POST http://localhost:5000/withdraw \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "id": "17", "amount": 10 }'
|
||||
```
|
||||
|
||||
```txt
|
||||
{"id":"17","balance":2}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Deposit Money**
|
||||
|
||||
```sh
|
||||
curl http://localhost:5000/17
|
||||
```
|
||||
|
||||
```txt
|
||||
{"id":"17","balance":2}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Withdraw Money (pubsub)**
|
||||
|
||||
```sh
|
||||
actions publish -t withdraw -p '{"id": "17", "amount": 15 }'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Deposit Money (pubsub)**
|
||||
|
||||
```sh
|
||||
actions publish -t deposit -p '{"id": "17", "amount": 15 }'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Samples
|
||||
|
||||
*All of the interesting code in this sample is in Startup.cs and Controllers/SampleController.cs*
|
||||
|
||||
```C#
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddControllers().AddDapr();
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`AddDapr()` registers the Dapr integration with controllers. This also registers the `StateClient` service with the dependency injection container. This service can be used to interact with the Dapr state-store.
|
||||
|
||||
---
|
||||
|
||||
```C#
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapSubscribeHandler();
|
||||
endpoints.MapControllers();
|
||||
});
|
||||
```
|
||||
|
||||
`MapSubscribeHandler()` registers an endpoint that will be called by the Dapr runtime to register for pub-sub topics. This is is not needed unless using pub-sub.
|
||||
|
||||
---
|
||||
|
||||
```C#
|
||||
[Topic("deposit")]
|
||||
[HttpPost("deposit")]
|
||||
public async Task<ActionResult<Account>> Deposit(...)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`[Topic(...)]` associates a pub-sub topic with this endpoint.
|
||||
|
||||
---
|
||||
|
||||
```C#
|
||||
[HttpGet("{account}")]
|
||||
public ActionResult<Account> Get(StateEntry<Account> account)
|
||||
{
|
||||
if (account.Value is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return account.Value;
|
||||
}
|
||||
```
|
||||
|
||||
Dapr's controller integration can automatically bind data from the state-store to an action parameter. Since the parameter's name is `account` the value of the account route-value will be used as the key. The data is stored in the state-store as JSON and will be deserialized as an object of type `Account`.
|
||||
|
||||
This could alternatively be written as:
|
||||
|
||||
```C#
|
||||
[HttpGet("{account}")]
|
||||
public ActionResult<Account> Get([FromState] Account account)
|
||||
{
|
||||
...
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public ActionResult<Account> Get([FromState("id")] Account account)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Using `[FromState]` allows binding a data type directly without using `StateEntry<>`. `[FromState(...)]` can also be used to specify which route-value contains the state-store key.
|
||||
|
||||
---
|
||||
|
||||
```C#
|
||||
[Topic("deposit")]
|
||||
[HttpPost("deposit")]
|
||||
public async Task<ActionResult<Account>> Deposit(Transaction transaction, [FromServices] StateClient stateClient)
|
||||
{
|
||||
var state = await stateClient.GetStateEntryAsync<Account>(transaction.Id);
|
||||
state.Value.Balance += transaction.Amount;
|
||||
await state.SaveAsync();
|
||||
return state.Value;
|
||||
}
|
||||
```
|
||||
|
||||
The `StateClient` can be retrieved from the dependency injection container, and can be used to imperatively access the state-store.
|
||||
|
||||
`state.SaveAsync()` can be used to save changes to a `StateEntry<>`.
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Dapr;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace ControllerSample
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddControllers().AddDapr();
|
||||
|
||||
services.AddSingleton(new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
});
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapSubscribeHandler();
|
||||
endpoints.MapControllers();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace ControllerSample
|
||||
{
|
||||
public class Transaction
|
||||
{
|
||||
[Required]
|
||||
public string Id { get; set; }
|
||||
|
||||
[Range(0, double.MaxValue)]
|
||||
public decimal Amount { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<Project>
|
||||
<Import Project="$(MSBuildThisFileDirectory)..\properties\dapr_managed_netcore.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace RoutingSample
|
||||
{
|
||||
public class Account
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public decimal Balance { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace RoutingSample
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:38427",
|
||||
"sslPort": 44370
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "api/values",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"RoutingSample": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "api/values",
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
# ASP.NET Core Routing Sample
|
||||
|
||||
This sample shows using Dapr with ASP.NET Core routing. This application is a simple and not-so-secure banking application. The application uses the Dapr state-store for its data storage.
|
||||
|
||||
It exposes the following endpoints over HTTP:
|
||||
- GET `/{id}`: Get the balance for the account specified by `id`
|
||||
- POST `/deposit`: Accepts a JSON payload to deposit money to an account
|
||||
- POST `/withdraw`: Accepts a JSON payload to withdraw money from an account
|
||||
|
||||
The application also registers for pub-sub with the `deposit` and `withdraw` topics.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
To run the sample locally run this comment in this directory:
|
||||
```sh
|
||||
actions run --app-id routing --app-port 5000 dotnet run
|
||||
```
|
||||
|
||||
The application will listen on port 5000 for HTTP.
|
||||
|
||||
### Examples
|
||||
|
||||
**Deposit Money**
|
||||
|
||||
```sh
|
||||
curl -X POST http://localhost:5000/deposit \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "id": "17", "amount": 12 }'
|
||||
```
|
||||
|
||||
```txt
|
||||
{"id":"17","balance":12}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Withdraw Money**
|
||||
|
||||
```sh
|
||||
curl -X POST http://localhost:5000/withdraw \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "id": "17", "amount": 10 }'
|
||||
```
|
||||
|
||||
```txt
|
||||
{"id":"17","balance":2}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Deposit Money**
|
||||
|
||||
```sh
|
||||
curl http://localhost:5000/17
|
||||
```
|
||||
|
||||
```txt
|
||||
{"id":"17","balance":2}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Withdraw Money (pubsub)**
|
||||
|
||||
```sh
|
||||
actions publish -t withdraw -p '{"id": "17", "amount": 15 }'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Deposit Money (pubsub)**
|
||||
|
||||
```sh
|
||||
actions publish -t deposit -p '{"id": "17", "amount": 15 }'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Samples
|
||||
|
||||
*All of the interesting code in this sample is in Startup.cs*
|
||||
|
||||
```C#
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddDaprClient();
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`AddDaprClient()` registers the `StateClient` service with the dependency injection container. This service can be used to interact with the Dapr state-store.
|
||||
|
||||
---
|
||||
|
||||
```C#
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapSubscribeHandler();
|
||||
|
||||
endpoints.MapGet("{id}", Balance);
|
||||
endpoints.MapPost("deposit", Deposit).WithTopic("deposit");
|
||||
endpoints.MapPost("withdraw", Withdraw).WithTopic("withdraw");
|
||||
});
|
||||
```
|
||||
|
||||
`MapSubscribeHandler()` registers an endpoint that will be called by the Dapr runtime to register for pub-sub topics. This is is not needed unless using pub-sub.
|
||||
|
||||
`MapGet(...)` and `MapPost(...)` are provided by ASP.NET Core routing - these are used to setup endpoints to handle HTTP requests.
|
||||
|
||||
`WithTopic(...)` associates an endpoint with a pub-sub topic.
|
||||
|
||||
---
|
||||
|
||||
```C#
|
||||
async Task Balance(HttpContext context)
|
||||
{
|
||||
var client = context.RequestServices.GetRequiredService<StateClient>();
|
||||
|
||||
var id = (string)context.Request.RouteValues["id"];
|
||||
var account = await client.GetStateAsync<Account>(id);
|
||||
if (account == null)
|
||||
{
|
||||
context.Response.StatusCode = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
context.Response.ContentType = "application/json";
|
||||
await JsonSerializer.SerializeAsync(context.Response.Body, account, serializerOptions);
|
||||
}
|
||||
```
|
||||
|
||||
Here `GetRequiredService<StateClient>()` is used to retrieve the `StateClient` from the service provider.
|
||||
|
||||
`client.GetStateAsync<Account>(id)` is used to retrieve an `Account` object from that state-store using the key in the variable `id`. The `Account` object stored in the state-store as JSON. If no entry is found for the specified key, then `null` will be returned.
|
||||
|
||||
---
|
||||
|
||||
```C#
|
||||
await client.SaveStateAsync(transaction.Id, account);
|
||||
```
|
||||
|
||||
`SaveStateAsync(...)` is used to save data to the state-store.
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Dapr.AspNetCore\Microsoft.Dapr.AspNetCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Dapr;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace RoutingSample
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddDaprClient();
|
||||
|
||||
services.AddSingleton(new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
});
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, JsonSerializerOptions serializerOptions)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapSubscribeHandler();
|
||||
|
||||
endpoints.MapGet("{id}", Balance);
|
||||
endpoints.MapPost("deposit", Deposit).WithTopic("deposit");
|
||||
endpoints.MapPost("withdraw", Withdraw).WithTopic("withdraw");
|
||||
});
|
||||
|
||||
async Task Balance(HttpContext context)
|
||||
{
|
||||
var client = context.RequestServices.GetRequiredService<StateClient>();
|
||||
|
||||
var id = (string)context.Request.RouteValues["id"];
|
||||
var account = await client.GetStateAsync<Account>(id);
|
||||
if (account == null)
|
||||
{
|
||||
context.Response.StatusCode = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
context.Response.ContentType = "application/json";
|
||||
await JsonSerializer.SerializeAsync(context.Response.Body, account, serializerOptions);
|
||||
}
|
||||
|
||||
async Task Deposit(HttpContext context)
|
||||
{
|
||||
var client = context.RequestServices.GetRequiredService<StateClient>();
|
||||
|
||||
var transaction = await JsonSerializer.DeserializeAsync<Transaction>(context.Request.Body, serializerOptions);
|
||||
var account = await client.GetStateAsync<Account>(transaction.Id);
|
||||
if (account == null)
|
||||
{
|
||||
account = new Account() { Id = transaction.Id, };
|
||||
}
|
||||
|
||||
if (transaction.Amount < 0m)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
account.Balance += transaction.Amount;
|
||||
await client.SaveStateAsync(transaction.Id, account);
|
||||
|
||||
context.Response.ContentType = "application/json";
|
||||
await JsonSerializer.SerializeAsync(context.Response.Body, account, serializerOptions);
|
||||
}
|
||||
|
||||
async Task Withdraw(HttpContext context)
|
||||
{
|
||||
var client = context.RequestServices.GetRequiredService<StateClient>();
|
||||
|
||||
var transaction = await JsonSerializer.DeserializeAsync<Transaction>(context.Request.Body, serializerOptions);
|
||||
var account = await client.GetStateAsync<Account>(transaction.Id);
|
||||
if (account == null)
|
||||
{
|
||||
context.Response.StatusCode = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
if (transaction.Amount < 0m)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
account.Balance -= transaction.Amount;
|
||||
await client.SaveStateAsync(transaction.Id, account);
|
||||
|
||||
context.Response.ContentType = "application/json";
|
||||
await JsonSerializer.SerializeAsync(context.Response.Body, account, serializerOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace RoutingSample
|
||||
{
|
||||
public class Transaction
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public decimal Amount { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<Project>
|
||||
<Import Project="$(MSBuildThisFileDirectory)..\properties\dapr_managed_netcore.props" />
|
||||
<Import Project="$(MSBuildThisFileDirectory)..\properties\dapr_managed_stylecop.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
|
||||
<!-- Stylecop needs the documentation file to exist -->
|
||||
<DocumentationFile>$(OutputPath)$(MSBuildProjectName).xml</DocumentationFile>
|
||||
|
||||
<!-- Nuget package properties when packed using dotnet pack. -->
|
||||
<NuspecFile>nuspec\$(MSBuildProjectName).nuspec</NuspecFile>
|
||||
<NuspecProperties>version=$(NupkgVersion);Configuration=$(Configuration)</NuspecProperties>
|
||||
|
||||
<!-- Disabling NU5128 Warning, https://docs.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu5128 -->
|
||||
<NoWarn>$(NoWarn);NU5128</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="StyleCop.Analyzers" Version="1.1.118">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,16 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\properties\dapr_managed_netcore.props" />
|
||||
<Import Project="..\..\properties\dapr_managed_stylecop.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
|
||||
|
||||
<!-- Nuget package properties when packed using dotnet pack. -->
|
||||
<NuspecFile>nuspec\Microsoft.Dapr.Actors.AspNetCore.nuspec</NuspecFile>
|
||||
<NuspecProperties>version=$(NupkgVersion);Configuration=$(Configuration)</NuspecProperties>
|
||||
<!-- Disabling NU5128 Warning, https://docs.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu5128 -->
|
||||
<NoWarn>$(NoWarn);NU5128</NoWarn>
|
||||
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
|
|
@ -18,12 +8,6 @@
|
|||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Dapr.Actors\Microsoft.Dapr.Actors.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Update="StyleCop.Analyzers" Version="1.1.118">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Resources\SR.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\properties\dapr_managed_netcore.props" />
|
||||
<Import Project="..\..\properties\dapr_managed_stylecop.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
|
||||
|
||||
<!-- Nuget package properties when packed using dotnet pack. -->
|
||||
<NuspecFile>nuspec\Microsoft.Dapr.Actors.nuspec</NuspecFile>
|
||||
<NuspecProperties>version=$(NupkgVersion);Configuration=$(Configuration)</NuspecProperties>
|
||||
<!-- Disabling NU5128 Warning, https://docs.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu5128 -->
|
||||
<NoWarn>$(NoWarn);NU5128</NoWarn>
|
||||
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
using System;
|
||||
using Microsoft.Dapr;
|
||||
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="IEndpointConventionBuilder" />.
|
||||
/// </summary>
|
||||
public static class DaprEndpointConventionBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds <see cref="TopicAttribute" /> metadata to the provided <see cref="IEndpointConventionBuilder" />.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IEndpointConventionBuilder" />.</param>
|
||||
/// <param name="name">The topic name.</param>
|
||||
/// <typeparam name="T">The <see cref="IEndpointConventionBuilder" /> type.</typeparam>
|
||||
/// <returns>The <see cref="IEndpointConventionBuilder" />.</returns>
|
||||
public static T WithTopic<T>(this T builder, string name)
|
||||
where T : IEndpointConventionBuilder
|
||||
{
|
||||
if (builder is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
throw new ArgumentException("The value cannot be null or empty.", nameof(name));
|
||||
}
|
||||
|
||||
builder.WithMetadata(new TopicAttribute(name));
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Dapr;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="IEndpointRouteBuilder" />.
|
||||
/// </summary>
|
||||
public static class DaprEndpointRouteBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps an endpoint that will respond to requests to <c>/actions/subscribe</c> from the
|
||||
/// Dapr runtime.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder" />.</param>
|
||||
/// <returns>The <see cref="IEndpointConventionBuilder" />.</returns>
|
||||
public static IEndpointConventionBuilder MapSubscribeHandler(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
if (endpoints is null)
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
|
||||
return endpoints.MapGet("actions/subscribe", async context =>
|
||||
{
|
||||
var dataSource = context.RequestServices.GetRequiredService<EndpointDataSource>();
|
||||
var entries = dataSource.Endpoints
|
||||
.Select(e => e.Metadata.GetMetadata<TopicAttribute>()?.Name)
|
||||
.Where(n => n != null)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
context.Response.ContentType = "application/json";
|
||||
await JsonSerializer.SerializeAsync(context.Response.Body, entries, context.RequestServices.GetService<JsonSerializerOptions>());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.Dapr;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="IMvcBuilder" />.
|
||||
/// </summary>
|
||||
public static class DaprMvcBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Dapr integration for MVC to the provided <see cref="IMvcBuilder" />.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IMvcBuilder" />.</param>
|
||||
public static void AddDapr(this IMvcBuilder builder)
|
||||
{
|
||||
if (builder is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
// This pattern prevents registering services multiple times in the case AddDapr is called
|
||||
// by non-user-code.
|
||||
if (builder.Services.Contains(ServiceDescriptor.Singleton<DaprMvcMarkerService, DaprMvcMarkerService>()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
builder.Services.AddDaprClient();
|
||||
|
||||
builder.Services.AddSingleton<DaprMvcMarkerService>();
|
||||
builder.Services.AddSingleton<IApplicationModelProvider, StateEntryApplicationModelProvider>();
|
||||
builder.Services.Configure<MvcOptions>(options =>
|
||||
{
|
||||
options.ModelBinderProviders.Insert(0, new StateEntryModelBinderProvider());
|
||||
});
|
||||
}
|
||||
|
||||
private class DaprMvcMarkerService
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.Dapr;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="IServiceCollection" />.
|
||||
/// </summary>
|
||||
public static class DaprServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Dapr client services to the provided <see cref="IServiceCollection" />. This does not include integration
|
||||
/// with ASP.NET Core MVC. Use the <c>AddDapr()</c> extension method on <c>IMvcBuilder</c> to register MVC integration.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection" />.</param>
|
||||
public static void AddDaprClient(this IServiceCollection services)
|
||||
{
|
||||
if (services is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
// This pattern prevents registering services multiple times in the case AddDaprClient is called
|
||||
// by non-user-code.
|
||||
if (services.Contains(ServiceDescriptor.Singleton<DaprClientMarkerService, DaprClientMarkerService>()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
services.AddSingleton<DaprClientMarkerService>();
|
||||
|
||||
// StateHttpClient can be used with or without JsonSerializerOptions registered
|
||||
// in DI. If the user registers JsonSerializerOptions, it will be picked up by the client automatically.
|
||||
services.AddHttpClient("state").AddTypedClient<StateClient, StateHttpClient>();
|
||||
}
|
||||
|
||||
private class DaprClientMarkerService
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Dapr;
|
||||
|
||||
/// <summary>
|
||||
/// Attributes a parameter or property as retrieved from the Dapr state store.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
|
||||
public class FromStateAttribute : Attribute, IBindingSourceMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="FromStateAttribute" />.
|
||||
/// </summary>
|
||||
public FromStateAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="FromStateAttribute" />.
|
||||
/// </summary>
|
||||
/// <param name="key">The state key.</param>
|
||||
public FromStateAttribute(string key)
|
||||
{
|
||||
this.Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state store key.
|
||||
/// </summary>
|
||||
public string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="BindingSource" />.
|
||||
/// </summary>
|
||||
public BindingSource BindingSource
|
||||
{
|
||||
get
|
||||
{
|
||||
return new FromStateBindingSource(this.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
internal class FromStateBindingSource : BindingSource
|
||||
{
|
||||
public FromStateBindingSource(string key)
|
||||
: base("state", "Dapr state store", isGreedy: true, isFromRequest: false)
|
||||
{
|
||||
this.Key = key;
|
||||
}
|
||||
|
||||
public string Key { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
|
||||
<ProjectReference Include="..\Microsoft.Dapr.Client\Microsoft.Dapr.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.Dapr.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr
|
||||
{
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
|
||||
internal class StateEntryApplicationModelProvider : IApplicationModelProvider
|
||||
{
|
||||
public int Order => 0;
|
||||
|
||||
public void OnProvidersExecuting(ApplicationModelProviderContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnProvidersExecuted(ApplicationModelProviderContext context)
|
||||
{
|
||||
// Run after default providers, and customize the binding source for StateEntry<>.
|
||||
foreach (var controller in context.Result.Controllers)
|
||||
{
|
||||
foreach (var property in controller.ControllerProperties)
|
||||
{
|
||||
if (property.BindingInfo == null)
|
||||
{
|
||||
// Not bindable.
|
||||
}
|
||||
else if (property.BindingInfo.BindingSource.Id == "state")
|
||||
{
|
||||
// Already configured, don't overwrite in case the user customized it.
|
||||
}
|
||||
else if (IsStateEntryType(property.ParameterType))
|
||||
{
|
||||
property.BindingInfo.BindingSource = new FromStateBindingSource(null);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var action in controller.Actions)
|
||||
{
|
||||
foreach (var parameter in action.Parameters)
|
||||
{
|
||||
if (parameter.BindingInfo.BindingSource.Id == "state")
|
||||
{
|
||||
// Already configured, don't overwrite in case the user customized it.
|
||||
}
|
||||
else if (IsStateEntryType(parameter.ParameterType))
|
||||
{
|
||||
parameter.BindingInfo.BindingSource = new FromStateBindingSource(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsStateEntryType(Type type)
|
||||
{
|
||||
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(StateEntry<>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
internal class StateEntryModelBinder : IModelBinder
|
||||
{
|
||||
private readonly Func<StateClient, string, Task<object>> thunk;
|
||||
private readonly string key;
|
||||
private readonly bool isStateEntry;
|
||||
private readonly Type type;
|
||||
|
||||
public StateEntryModelBinder(string key, bool isStateEntry, Type type)
|
||||
{
|
||||
this.key = key;
|
||||
this.isStateEntry = isStateEntry;
|
||||
this.type = type;
|
||||
|
||||
if (isStateEntry)
|
||||
{
|
||||
var method = this.GetType().GetMethod(nameof(GetStateEntryAsync), BindingFlags.Static | BindingFlags.NonPublic);
|
||||
method = method.MakeGenericMethod(type);
|
||||
this.thunk = (Func<StateClient, string, Task<object>>)Delegate.CreateDelegate(typeof(Func<StateClient, string, Task<object>>), null, method);
|
||||
}
|
||||
else
|
||||
{
|
||||
var method = this.GetType().GetMethod(nameof(GetStateAsync), BindingFlags.Static | BindingFlags.NonPublic);
|
||||
method = method.MakeGenericMethod(type);
|
||||
this.thunk = (Func<StateClient, string, Task<object>>)Delegate.CreateDelegate(typeof(Func<StateClient, string, Task<object>>), null, method);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
{
|
||||
if (bindingContext is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(bindingContext));
|
||||
}
|
||||
|
||||
var stateClient = bindingContext.HttpContext.RequestServices.GetRequiredService<StateClient>();
|
||||
|
||||
// Look up route values to use for keys into state.
|
||||
bool missingKey = false;
|
||||
var keyName = this.key ?? bindingContext.FieldName;
|
||||
var key = (string)bindingContext.HttpContext.Request.RouteValues[keyName];
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
missingKey = true;
|
||||
}
|
||||
|
||||
if (missingKey)
|
||||
{
|
||||
// If we get here this is a configuration error. The error is somewhat opaque on
|
||||
// purpose to avoid leaking too much information about the app.
|
||||
var message = $"Required value {key} not present.";
|
||||
bindingContext.Result = ModelBindingResult.Failed();
|
||||
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, message);
|
||||
return;
|
||||
}
|
||||
|
||||
var obj = await this.thunk(stateClient, key);
|
||||
bindingContext.Result = ModelBindingResult.Success(obj);
|
||||
|
||||
bindingContext.ValidationState.Add(bindingContext.Result.Model, new ValidationStateEntry()
|
||||
{
|
||||
// Don't do validation since the data came from a trusted source.
|
||||
SuppressValidation = true,
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<object> GetStateEntryAsync<T>(StateClient stateClient, string key)
|
||||
{
|
||||
return await stateClient.GetStateEntryAsync<T>(key);
|
||||
}
|
||||
|
||||
private static async Task<object> GetStateAsync<T>(StateClient stateClient, string key)
|
||||
{
|
||||
return await stateClient.GetStateAsync<T>(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr
|
||||
{
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
internal class StateEntryModelBinderProvider : IModelBinderProvider
|
||||
{
|
||||
public IModelBinder GetBinder(ModelBinderProviderContext context)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (!CanBind(context, out var type))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var key = (context.BindingInfo.BindingSource as FromStateBindingSource)?.Key;
|
||||
return new StateEntryModelBinder(key, type != context.Metadata.ModelType, type);
|
||||
}
|
||||
|
||||
private static bool CanBind(ModelBinderProviderContext context, out Type type)
|
||||
{
|
||||
if (context.BindingInfo.BindingSource.Id == "state")
|
||||
{
|
||||
// [FromState]
|
||||
type = Unwrap(context.Metadata.ModelType);
|
||||
return true;
|
||||
}
|
||||
|
||||
type = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Type Unwrap(Type type)
|
||||
{
|
||||
if (type.IsGenericType &&
|
||||
type.GetGenericTypeDefinition() == typeof(StateEntry<>))
|
||||
{
|
||||
return type.GetGenericArguments()[0];
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Metadata that describes an endpoint as a subscriber to a topic.
|
||||
/// </summary>
|
||||
public class TopicAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="TopicAttribute" />.
|
||||
/// </summary>
|
||||
/// <param name="name">The topic name.</param>
|
||||
public TopicAttribute(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
throw new ArgumentException("The value cannot be null or empty.", nameof(name));
|
||||
}
|
||||
|
||||
this.Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the topic name.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2011/10/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>Microsoft.Dapr.AspNetCore</id>
|
||||
<version>$version$</version>
|
||||
<title>Microsoft.Dapr.AspNetCore</title>
|
||||
<authors>Microsoft</authors>
|
||||
<owners>Microsoft,Dapr</owners>
|
||||
<license type="expression">MIT</license>
|
||||
<projectUrl>http://aka.ms/actions</projectUrl>
|
||||
<icon>images\nuget_icon_ms.png</icon>
|
||||
<requireLicenseAcceptance>true</requireLicenseAcceptance>
|
||||
<description>This package contains the reference assemblies for developing services using Dapr and AspNetCore.</description>
|
||||
<summary>This package contains the reference assemblies for developing services using Dapr.</summary>
|
||||
<copyright>© Microsoft Corporation. All rights reserved.</copyright>
|
||||
<tags>Microsoft Azure Dapr</tags>
|
||||
<dependencies>
|
||||
<dependency id="Microsoft.Dapr.Client" version="[$version$]" />
|
||||
</dependencies>
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="..\..\..\properties\nuget_icon_ms.png" target="images\" />
|
||||
<file src="..\..\..\bin\$Configuration$\Microsoft.Dapr.AspNetCore.dll" target="lib\netcoreapp3.0" exclude="" />
|
||||
<file src="..\..\..\bin\$Configuration$\Microsoft.Dapr.AspNetCore.xml" target="lib\netcoreapp3.0" exclude="" />
|
||||
</files>
|
||||
</package>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr
|
||||
{
|
||||
using System;
|
||||
|
||||
internal static class DaprUris
|
||||
{
|
||||
public const string StatePath = "/v1.0/state";
|
||||
|
||||
public static string DefaultPort => Environment.GetEnvironmentVariable("ACTIONS_PORT") ?? "3500";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Text.Json" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr
|
||||
{
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// A client for interacting with the Dapr state store.
|
||||
/// </summary>
|
||||
public abstract class StateClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current value associated with the <paramref name="key" /> from the Dapr state store.
|
||||
/// </summary>
|
||||
/// <param name="key">The state key.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
|
||||
/// <typeparam name="TValue">The data type.</typeparam>
|
||||
/// <returns>A <see cref="ValueTask" /> that will return the value when the operation has completed.</returns>
|
||||
public abstract ValueTask<TValue> GetStateAsync<TValue>(string key, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="StateEntry{T}" /> for the current value associated with the <paramref name="key" /> from
|
||||
/// the Dapr state store.
|
||||
/// </summary>
|
||||
/// <param name="key">The state key.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
|
||||
/// <typeparam name="TValue">The data type.</typeparam>
|
||||
/// <returns>A <see cref="ValueTask" /> that will return the <see cref="StateEntry{T}" /> when the operation has completed.</returns>
|
||||
public async ValueTask<StateEntry<TValue>> GetStateEntryAsync<TValue>(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
throw new ArgumentException("The value cannot be null or empty.", nameof(key));
|
||||
}
|
||||
|
||||
var value = await this.GetStateAsync<TValue>(key, cancellationToken);
|
||||
return new StateEntry<TValue>(this, key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the provided <paramref name="value" /> associated with the provided <paramref name="key" /> to the Dapr state
|
||||
/// store.
|
||||
/// </summary>
|
||||
/// <param name="key">The state key.</param>
|
||||
/// <param name="value">The value to save.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
|
||||
/// <typeparam name="TValue">The data type.</typeparam>
|
||||
/// <returns>A <see cref="ValueTask" /> that will complete when the operation has completed.</returns>
|
||||
public abstract ValueTask SaveStateAsync<TValue>(string key, TValue value, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr
|
||||
{
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a value in the Dapr state store.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The data type.</typeparam>
|
||||
public sealed class StateEntry<TValue>
|
||||
{
|
||||
private readonly StateClient client;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="StateEntry{T}" /> instance.
|
||||
/// </summary>
|
||||
/// <param name="client">The <see cref="StateClient" /> instance used to retrieve the value.</param>
|
||||
/// <param name="key">The state key.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <remarks>
|
||||
/// Application code should not need to create instances of <see cref="StateEntry{T}" />. Use
|
||||
/// <see cref="StateClient.GetStateEntryAsync{TValue}(string, CancellationToken)" /> to access
|
||||
/// state entries.
|
||||
/// </remarks>
|
||||
public StateEntry(StateClient client, string key, TValue value)
|
||||
{
|
||||
if (client is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
throw new ArgumentException("The value cannot be null or empty.", nameof(key));
|
||||
}
|
||||
|
||||
this.Key = key;
|
||||
this.Value = value;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state key.
|
||||
/// </summary>
|
||||
public string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value.
|
||||
/// </summary>
|
||||
public TValue Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Saves the the current value of <see cref="Value" /> to the state store.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
|
||||
/// <returns>A <see cref="Task" /> that will complete when the operation has completed.</returns>
|
||||
public ValueTask SaveAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.client.SaveStateAsync(this.Key, this.Value, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr
|
||||
{
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using static Microsoft.Dapr.DaprUris;
|
||||
|
||||
/// <summary>
|
||||
/// A client for interacting with the Dapr state store using <see cref="HttpClient" />.
|
||||
/// </summary>
|
||||
public sealed class StateHttpClient : StateClient
|
||||
{
|
||||
private readonly HttpClient client;
|
||||
private readonly JsonSerializerOptions serializerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="StateHttpClient" />.
|
||||
/// </summary>
|
||||
/// <param name="client">The <see cref="HttpClient" />.</param>
|
||||
/// <param name="serializerOptions">The <see cref="JsonSerializerOptions" />.</param>
|
||||
public StateHttpClient(HttpClient client, JsonSerializerOptions serializerOptions = null)
|
||||
{
|
||||
if (client is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
this.client = client;
|
||||
this.serializerOptions = serializerOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value associated with the <paramref name="key" /> from the Dapr state store.
|
||||
/// </summary>
|
||||
/// <param name="key">The state key.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
|
||||
/// <typeparam name="TValue">The data type.</typeparam>
|
||||
/// <returns>A <see cref="ValueTask" /> that will return the value when the operation has completed.</returns>
|
||||
public async override ValueTask<TValue> GetStateAsync<TValue>(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
throw new ArgumentException("The value cannot be null or empty.", nameof(key));
|
||||
}
|
||||
|
||||
var url = this.client.BaseAddress == null ? $"http://localhost:{DefaultPort}{StatePath}/{key}" : $"{StatePath}{key}";
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
var response = await this.client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode && response.Content != null)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
throw new HttpRequestException($"Failed to get state with status code '{response.StatusCode}': {error}.");
|
||||
}
|
||||
else if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to get state with status code '{response.StatusCode}'.");
|
||||
}
|
||||
|
||||
if (response.Content == null || response.Content.Headers?.ContentLength == 0)
|
||||
{
|
||||
// The state store will return empty application/json instead of 204/404.
|
||||
return default;
|
||||
}
|
||||
|
||||
using (var stream = await response.Content.ReadAsStreamAsync())
|
||||
{
|
||||
return await JsonSerializer.DeserializeAsync<TValue>(stream, this.serializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the provided <paramref name="value" /> associated with the provided <paramref name="key" /> to the Dapr state
|
||||
/// store.
|
||||
/// </summary>
|
||||
/// <param name="key">The state key.</param>
|
||||
/// <param name="value">The value to save.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
|
||||
/// <typeparam name="TValue">The data type.</typeparam>
|
||||
/// <returns>A <see cref="ValueTask" /> that will complete when the operation has completed.</returns>
|
||||
public async override ValueTask SaveStateAsync<TValue>(string key, TValue value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
throw new ArgumentException("The value cannot be null or empty.", nameof(key));
|
||||
}
|
||||
|
||||
var url = this.client.BaseAddress == null ? $"http://localhost:{DefaultPort}{StatePath}" : StatePath;
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
var obj = new object[] { new { key = key, value = value, } };
|
||||
request.Content = CreateContent(obj, this.serializerOptions);
|
||||
|
||||
var response = await this.client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode && response.Content != null)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
throw new HttpRequestException($"Failed to get state with status code '{response.StatusCode}': {error}.");
|
||||
}
|
||||
else if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to get state with status code '{response.StatusCode}'.");
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static AsyncJsonContent<T> CreateContent<T>(T obj, JsonSerializerOptions serializerOptions)
|
||||
{
|
||||
return new AsyncJsonContent<T>(obj, serializerOptions);
|
||||
}
|
||||
|
||||
// Note: using push-streaming content here has a little higher cost for trivially-size payloads,
|
||||
// but avoids the significant allocation overhead in the cases where the content is really large.
|
||||
//
|
||||
// Similar to https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Net.Http.Formatting/PushStreamContent.cs
|
||||
// but simplified because of async.
|
||||
private class AsyncJsonContent<T> : HttpContent
|
||||
{
|
||||
private readonly T obj;
|
||||
private readonly JsonSerializerOptions serializerOptions;
|
||||
|
||||
public AsyncJsonContent(T obj, JsonSerializerOptions serializerOptions)
|
||||
{
|
||||
this.obj = obj;
|
||||
this.serializerOptions = serializerOptions;
|
||||
|
||||
this.Headers.ContentType = new MediaTypeHeaderValue("application/json") { CharSet = "UTF-8", };
|
||||
}
|
||||
|
||||
protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
|
||||
{
|
||||
return JsonSerializer.SerializeAsync(stream, this.obj, this.serializerOptions);
|
||||
}
|
||||
|
||||
protected override bool TryComputeLength(out long length)
|
||||
{
|
||||
// We can't know the length of the content being pushed to the output stream without doing
|
||||
// some writing.
|
||||
//
|
||||
// If we want to optimize this case, it could be done by implementing a custom stream
|
||||
// and then doing the first write to a fixed-size pooled byte array.
|
||||
//
|
||||
// HTTP is slightly more efficient when you can avoid using chunking (need to know Content-Length)
|
||||
// up front.
|
||||
length = -1;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2011/10/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>Microsoft.Dapr.Client</id>
|
||||
<version>$version$</version>
|
||||
<title>Microsoft.Dapr.Client</title>
|
||||
<authors>Microsoft</authors>
|
||||
<owners>Microsoft,Dapr</owners>
|
||||
<license type="expression">MIT</license>
|
||||
<projectUrl>http://aka.ms/actions</projectUrl>
|
||||
<icon>images\nuget_icon_ms.png</icon>
|
||||
<requireLicenseAcceptance>true</requireLicenseAcceptance>
|
||||
<description>This package contains the reference assemblies for developing services using Dapr.</description>
|
||||
<summary>This package contains the reference assemblies for developing services using Dapr.</summary>
|
||||
<copyright>© Microsoft Corporation. All rights reserved.</copyright>
|
||||
<tags>Microsoft Azure Dapr</tags>
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="..\..\..\properties\nuget_icon_ms.png" target="images\" />
|
||||
<file src="..\..\..\bin\$Configuration$\Microsoft.Dapr.Client.dll" target="lib\netstandard2.0" exclude="" />
|
||||
<file src="..\..\..\bin\$Configuration$\Microsoft.Dapr.Client.xml" target="lib\netstandard2.0" exclude="" />
|
||||
</files>
|
||||
</package>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<Project>
|
||||
<Import Project="$(MSBuildThisFileDirectory)..\properties\dapr_managed_netcore.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\properties\dapr_managed_netcore.props" />
|
||||
<Import Project="..\..\properties\dapr_managed_stylecop.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Microsoft.Dapr.AspNetCore.IntegrationTest.App
|
||||
{
|
||||
[ApiController]
|
||||
public class DaprController : ControllerBase
|
||||
{
|
||||
[Topic("B")]
|
||||
[HttpPost("/topic-b")]
|
||||
public void TopicB()
|
||||
{
|
||||
}
|
||||
|
||||
[HttpPost("/controllerwithoutstateentry/{widget}")]
|
||||
public async Task AddOneWithoutStateEntry([FromServices]StateClient state, [FromState] Widget widget)
|
||||
{
|
||||
widget.Count++;
|
||||
await state.SaveStateAsync((string)HttpContext.Request.RouteValues["widget"], widget);
|
||||
}
|
||||
|
||||
[HttpPost("/controllerwithstateentry/{widget}")]
|
||||
public async Task AddOneWithStateEntry(StateEntry<Widget> widget)
|
||||
{
|
||||
widget.Value.Count++;
|
||||
await widget.SaveAsync();
|
||||
}
|
||||
|
||||
[HttpPost("/controllerwithstateentryandcustomkey/{widget}")]
|
||||
public async Task AddOneWithStateEntryAndCustomKey([FromState("widget")] StateEntry<Widget> state)
|
||||
{
|
||||
state.Value.Count++;
|
||||
await state.SaveAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Dapr.AspNetCore\Microsoft.Dapr.AspNetCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.Dapr.AspNetCore.IntegrationTest.App
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:14916",
|
||||
"sslPort": 44351
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "api/values",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"Microsoft.Dapr.AspNetCore.IntegrationTest.App": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "api/values",
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.Dapr.AspNetCore.IntegrationTest.App
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddControllers().AddDapr();
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapSubscribeHandler();
|
||||
endpoints.MapControllers();
|
||||
|
||||
endpoints.MapPost("/topic-a", context => Task.CompletedTask).WithTopic("A");
|
||||
|
||||
endpoints.MapPost("/routingwithstateentry/{widget}", async context =>
|
||||
{
|
||||
var stateClient = context.RequestServices.GetRequiredService<StateClient>();
|
||||
var state = await stateClient.GetStateEntryAsync<Widget>((string)context.Request.RouteValues["widget"]);
|
||||
state.Value.Count++;
|
||||
await state.SaveAsync();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr.AspNetCore.IntegrationTest.App
|
||||
{
|
||||
public class Widget
|
||||
{
|
||||
public string Size { get; set; }
|
||||
|
||||
public int Count { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr.AspNetCore.IntegrationTest
|
||||
{
|
||||
using Microsoft.Dapr.AspNetCore.IntegrationTest.App;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
public class AppWebApplicationFactory : WebApplicationFactory<Startup>
|
||||
{
|
||||
public StateTestClient StateClient { get; } = new StateTestClient();
|
||||
|
||||
protected override IHostBuilder CreateHostBuilder()
|
||||
{
|
||||
var builder = base.CreateHostBuilder();
|
||||
return builder.ConfigureServices((context, services) =>
|
||||
{
|
||||
services.AddSingleton<StateClient>(StateClient);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr.AspNetCore.IntegrationTest
|
||||
{
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Dapr.AspNetCore.IntegrationTest.App;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
[TestClass]
|
||||
public class ControllerIntegrationTest
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task ModelBinder_CanBindFromState()
|
||||
{
|
||||
using (var factory = new AppWebApplicationFactory())
|
||||
{
|
||||
var httpClient = factory.CreateClient();
|
||||
var stateClient = factory.StateClient;
|
||||
|
||||
await stateClient.SaveStateAsync("test", new Widget() { Size = "small", Count = 17, });
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/controllerwithoutstateentry/test");
|
||||
var response = await httpClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var widget = await stateClient.GetStateAsync<Widget>("test");
|
||||
widget.Count.Should().Be(18);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ModelBinder_CanBindFromState_WithStateEntry()
|
||||
{
|
||||
using (var factory = new AppWebApplicationFactory())
|
||||
{
|
||||
var httpClient = factory.CreateClient();
|
||||
var stateClient = factory.StateClient;
|
||||
|
||||
await stateClient.SaveStateAsync("test", new Widget() { Size = "small", Count = 17, });
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/controllerwithstateentry/test");
|
||||
var response = await httpClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var widget = await stateClient.GetStateAsync<Widget>("test");
|
||||
widget.Count.Should().Be(18);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ModelBinder_CanBindFromState_WithStateEntryAndCustomKey()
|
||||
{
|
||||
using (var factory = new AppWebApplicationFactory())
|
||||
{
|
||||
var httpClient = factory.CreateClient();
|
||||
var stateClient = factory.StateClient;
|
||||
|
||||
await stateClient.SaveStateAsync("test", new Widget() { Size = "small", Count = 17, });
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/controllerwithstateentryandcustomkey/test");
|
||||
var response = await httpClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var widget = await stateClient.GetStateAsync<Widget>("test");
|
||||
widget.Count.Should().Be(18);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="5.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Dapr.AspNetCore\Microsoft.Dapr.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.Dapr.AspNetCore.IntegrationTest.App\Microsoft.Dapr.AspNetCore.IntegrationTest.App.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Shared\TestHttpClient.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr.AspNetCore.IntegrationTest
|
||||
{
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Dapr.AspNetCore.IntegrationTest.App;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
[TestClass]
|
||||
public class RoutingIntegrationTest
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task StateClient_CanBindFromState()
|
||||
{
|
||||
using (var factory = new AppWebApplicationFactory())
|
||||
{
|
||||
var httpClient = factory.CreateClient();
|
||||
var stateClient = factory.StateClient;
|
||||
|
||||
await stateClient.SaveStateAsync("test", new Widget() { Size = "small", Count = 17, });
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/routingwithstateentry/test");
|
||||
var response = await httpClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var widget = await stateClient.GetStateAsync<Widget>("test");
|
||||
widget.Count.Should().Be(18);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
public class StateTestClient : StateClient
|
||||
{
|
||||
public Dictionary<string, object> State { get; } = new Dictionary<string, object>();
|
||||
|
||||
public override ValueTask<TValue> GetStateAsync<TValue>(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (State.TryGetValue(key, out var obj))
|
||||
{
|
||||
return new ValueTask<TValue>((TValue)obj);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ValueTask<TValue>(default(TValue));
|
||||
}
|
||||
}
|
||||
|
||||
public override ValueTask SaveStateAsync<TValue>(string key, TValue value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
State.Remove(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
State[key] = value;
|
||||
}
|
||||
|
||||
return new ValueTask(Task.CompletedTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr.AspNetCore.IntegrationTest
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
[TestClass]
|
||||
public class SubscribeEndpointTest
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task SubscribeEndpoint_ReportsTopics()
|
||||
{
|
||||
using (var factory = new AppWebApplicationFactory())
|
||||
{
|
||||
var httpClient = factory.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/actions/subscribe");
|
||||
var response = await httpClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using (var stream = await response.Content.ReadAsStreamAsync())
|
||||
{
|
||||
var json = await JsonSerializer.DeserializeAsync<JsonElement>(stream);
|
||||
|
||||
json.ValueKind.Should().Be(JsonValueKind.Array);
|
||||
json.GetArrayLength().Should().Be(2);
|
||||
var topics = new List<string>();
|
||||
foreach (var element in json.EnumerateArray())
|
||||
{
|
||||
topics.Add(element.GetString());
|
||||
}
|
||||
topics.Should().Contain("A");
|
||||
topics.Should().Contain("B");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="5.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Dapr.AspNetCore\Microsoft.Dapr.AspNetCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Shared\TestHttpClient.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr.AspNetCore.Test
|
||||
{
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using FluentAssertions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
|
||||
[TestClass]
|
||||
public class StateEntryModelBinderTest
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task BindAsync_WithoutMatchingRouteValue_ReportsError()
|
||||
{
|
||||
var binder = new StateEntryModelBinder("test", isStateEntry: false, typeof(Widget));
|
||||
|
||||
var httpClient = new TestHttpClient();
|
||||
var context = CreateContext(CreateServices(httpClient));
|
||||
|
||||
await binder.BindModelAsync(context);
|
||||
context.Result.IsModelSet.Should().BeFalse();
|
||||
context.ModelState.ErrorCount.Should().Be(1);
|
||||
context.ModelState["testParameter"].Errors.Count.Should().Be(1);
|
||||
|
||||
httpClient.Requests.Count.Should().Be(0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task BindAsync_CanBindValue()
|
||||
{
|
||||
var binder = new StateEntryModelBinder("id", isStateEntry: false, typeof(Widget));
|
||||
|
||||
var httpClient = new TestHttpClient();
|
||||
var context = CreateContext(CreateServices(httpClient));
|
||||
context.HttpContext.Request.RouteValues["id"] = "test";
|
||||
|
||||
var task = binder.BindModelAsync(context);
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.RespondWithJson(new Widget() { Size = "small", Color = "yellow", });
|
||||
|
||||
await task;
|
||||
context.Result.IsModelSet.Should().BeTrue();
|
||||
context.Result.Model.As<Widget>().Size.Should().Be("small");
|
||||
context.Result.Model.As<Widget>().Color.Should().Be("yellow");
|
||||
|
||||
context.ValidationState.Count.Should().Be(1);
|
||||
context.ValidationState[context.Result.Model].SuppressValidation.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task BindAsync_CanBindStateEntry()
|
||||
{
|
||||
var binder = new StateEntryModelBinder("id", isStateEntry: true, typeof(Widget));
|
||||
|
||||
var httpClient = new TestHttpClient();
|
||||
var context = CreateContext(CreateServices(httpClient));
|
||||
context.HttpContext.Request.RouteValues["id"] = "test";
|
||||
|
||||
var task = binder.BindModelAsync(context);
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.RespondWithJson(new Widget() { Size = "small", Color = "yellow", });
|
||||
|
||||
await task;
|
||||
context.Result.IsModelSet.Should().BeTrue();
|
||||
context.Result.Model.As<StateEntry<Widget>>().Key.Should().Be("test");
|
||||
context.Result.Model.As<StateEntry<Widget>>().Value.Size.Should().Be("small");
|
||||
context.Result.Model.As<StateEntry<Widget>>().Value.Color.Should().Be("yellow");
|
||||
|
||||
context.ValidationState.Count.Should().Be(1);
|
||||
context.ValidationState[context.Result.Model].SuppressValidation.Should().BeTrue();
|
||||
}
|
||||
|
||||
private static ModelBindingContext CreateContext(IServiceProvider services)
|
||||
{
|
||||
return new DefaultModelBindingContext()
|
||||
{
|
||||
ActionContext = new ActionContext()
|
||||
{
|
||||
HttpContext = new DefaultHttpContext()
|
||||
{
|
||||
RequestServices = services,
|
||||
},
|
||||
},
|
||||
ModelState = new ModelStateDictionary(),
|
||||
ModelName = "testParameter",
|
||||
ValidationState = new ValidationStateDictionary(),
|
||||
};
|
||||
}
|
||||
|
||||
private static IServiceProvider CreateServices(TestHttpClient client)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<StateClient>(new StateHttpClient(client, new JsonSerializerOptions()));
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private class Widget
|
||||
{
|
||||
public string Size { get; set; }
|
||||
|
||||
public string Color { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="5.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Dapr.Client\Microsoft.Dapr.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Shared\TestHttpClient.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr.Client.Test
|
||||
{
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
[TestClass]
|
||||
public class StateHttpClientTest
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task GetStateAsync_CanReadState()
|
||||
{
|
||||
var httpClient = new TestHttpClient();
|
||||
var client = new StateHttpClient(httpClient, new JsonSerializerOptions());
|
||||
|
||||
var task = client.GetStateAsync<Widget>("test");
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(GetStateUrl(3500, "test"));
|
||||
|
||||
entry.RespondWithJson(new Widget() { Size = "small", Color = "yellow", });
|
||||
|
||||
var state = await task;
|
||||
state.Size.Should().Be("small");
|
||||
state.Color.Should().Be("yellow");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetStateAsync_CanReadEmptyState_ReturnsDefault()
|
||||
{
|
||||
var httpClient = new TestHttpClient();
|
||||
var client = new StateHttpClient(httpClient, new JsonSerializerOptions());
|
||||
|
||||
var task = client.GetStateAsync<Widget>("test");
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(GetStateUrl(3500, "test"));
|
||||
|
||||
entry.Respond(new HttpResponseMessage(HttpStatusCode.OK));
|
||||
|
||||
var state = await task;
|
||||
state.Should().BeNull();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetStateAsync_ThrowsForNonSuccess()
|
||||
{
|
||||
var httpClient = new TestHttpClient();
|
||||
var client = new StateHttpClient(httpClient, new JsonSerializerOptions());
|
||||
|
||||
var task = client.GetStateAsync<Widget>("test");
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(GetStateUrl(3500, "test"));
|
||||
|
||||
entry.Respond(new HttpResponseMessage(HttpStatusCode.NotAcceptable));
|
||||
|
||||
await FluentActions.Awaiting(async () => await task).Should().ThrowAsync<HttpRequestException>();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SaveStateAsync_CanSaveState()
|
||||
{
|
||||
var httpClient = new TestHttpClient();
|
||||
var client = new StateHttpClient(httpClient, new JsonSerializerOptions());
|
||||
|
||||
var widget = new Widget() { Size = "small", Color = "yellow", };
|
||||
var task = client.SaveStateAsync("test", widget);
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(SaveStateUrl(3500));
|
||||
|
||||
using (var stream = await entry.Request.Content.ReadAsStreamAsync())
|
||||
{
|
||||
var json = await JsonSerializer.DeserializeAsync<JsonElement>(stream);
|
||||
json.ValueKind.Should().Be(JsonValueKind.Array);
|
||||
json.GetArrayLength().Should().Be(1);
|
||||
json[0].GetProperty("key").GetString().Should().Be("test");
|
||||
|
||||
var value = JsonSerializer.Deserialize<Widget>(json[0].GetProperty("value").GetRawText());
|
||||
value.Size.Should().Be("small");
|
||||
value.Color.Should().Be("yellow");
|
||||
}
|
||||
|
||||
entry.Respond(new HttpResponseMessage(HttpStatusCode.NoContent));
|
||||
await task;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SaveStateAsync_CanClearState()
|
||||
{
|
||||
var httpClient = new TestHttpClient();
|
||||
var client = new StateHttpClient(httpClient, new JsonSerializerOptions());
|
||||
|
||||
var task = client.SaveStateAsync<object>("test", null);
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(SaveStateUrl(3500));
|
||||
|
||||
using (var stream = await entry.Request.Content.ReadAsStreamAsync())
|
||||
{
|
||||
var json = await JsonSerializer.DeserializeAsync<JsonElement>(stream);
|
||||
json.ValueKind.Should().Be(JsonValueKind.Array);
|
||||
json.GetArrayLength().Should().Be(1);
|
||||
json[0].GetProperty("key").GetString().Should().Be("test");
|
||||
|
||||
json[0].GetProperty("value").ValueKind.Should().Be(JsonValueKind.Null);
|
||||
}
|
||||
|
||||
entry.Respond(new HttpResponseMessage(HttpStatusCode.NoContent));
|
||||
await task;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SetStateAsync_ThrowsForNonSuccess()
|
||||
{
|
||||
var httpClient = new TestHttpClient();
|
||||
var client = new StateHttpClient(httpClient, new JsonSerializerOptions());
|
||||
|
||||
var widget = new Widget() { Size = "small", Color = "yellow", };
|
||||
var task = client.SaveStateAsync("test", widget);
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(SaveStateUrl(3500));
|
||||
|
||||
entry.Respond(new HttpResponseMessage(HttpStatusCode.NotAcceptable));
|
||||
|
||||
await FluentActions.Awaiting(async () => await task).Should().ThrowAsync<HttpRequestException>();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetStateEntryAsync_CanReadState()
|
||||
{
|
||||
var httpClient = new TestHttpClient();
|
||||
var client = new StateHttpClient(httpClient, new JsonSerializerOptions());
|
||||
|
||||
var task = client.GetStateEntryAsync<Widget>("test");
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(GetStateUrl(3500, "test"));
|
||||
|
||||
entry.RespondWithJson(new Widget() { Size = "small", Color = "yellow", });
|
||||
|
||||
var state = await task;
|
||||
state.Key.Should().Be("test");
|
||||
state.Value.Size.Should().Be("small");
|
||||
state.Value.Color.Should().Be("yellow");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetStateEntryAsync_CanReadEmptyState_ReturnsDefault()
|
||||
{
|
||||
var httpClient = new TestHttpClient();
|
||||
var client = new StateHttpClient(httpClient, new JsonSerializerOptions());
|
||||
|
||||
var task = client.GetStateEntryAsync<Widget>("test");
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(GetStateUrl(3500, "test"));
|
||||
|
||||
entry.Respond(new HttpResponseMessage(HttpStatusCode.OK));
|
||||
|
||||
var state = await task;
|
||||
state.Key.Should().Be("test");
|
||||
state.Value.Should().BeNull();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetStateEntryAsync_CanSaveState()
|
||||
{
|
||||
var httpClient = new TestHttpClient();
|
||||
var client = new StateHttpClient(httpClient, new JsonSerializerOptions());
|
||||
|
||||
var task = client.GetStateEntryAsync<Widget>("test");
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(GetStateUrl(3500, "test"));
|
||||
|
||||
entry.RespondWithJson(new Widget() { Size = "small", Color = "yellow", });
|
||||
|
||||
var state = await task;
|
||||
state.Key.Should().Be("test");
|
||||
state.Value.Size.Should().Be("small");
|
||||
state.Value.Color.Should().Be("yellow");
|
||||
|
||||
state.Value.Color = "green";
|
||||
var task2 = state.SaveAsync();
|
||||
|
||||
httpClient.Requests.TryDequeue(out entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(SaveStateUrl(3500));
|
||||
|
||||
using (var stream = await entry.Request.Content.ReadAsStreamAsync())
|
||||
{
|
||||
var json = await JsonSerializer.DeserializeAsync<JsonElement>(stream);
|
||||
json.ValueKind.Should().Be(JsonValueKind.Array);
|
||||
json.GetArrayLength().Should().Be(1);
|
||||
json[0].GetProperty("key").GetString().Should().Be("test");
|
||||
|
||||
var value = JsonSerializer.Deserialize<Widget>(json[0].GetProperty("value").GetRawText());
|
||||
value.Size.Should().Be("small");
|
||||
value.Color.Should().Be("green");
|
||||
}
|
||||
|
||||
entry.Respond(new HttpResponseMessage(HttpStatusCode.NoContent));
|
||||
await task;
|
||||
}
|
||||
|
||||
private static string GetStateUrl(int port, string key)
|
||||
{
|
||||
return $"http://localhost:{port}/v1.0/state/{key}";
|
||||
}
|
||||
|
||||
private static string SaveStateUrl(int port)
|
||||
{
|
||||
return $"http://localhost:{port}/v1.0/state";
|
||||
}
|
||||
|
||||
private class Widget
|
||||
{
|
||||
public string Size { get; set; }
|
||||
|
||||
public string Color { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Dapr
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
// This client will capture all requests, and put them in .Requests for you to inspect.
|
||||
public class TestHttpClient : HttpClient
|
||||
{
|
||||
private readonly TestHttpClientHandler handler;
|
||||
|
||||
public TestHttpClient()
|
||||
: this(new TestHttpClientHandler())
|
||||
{
|
||||
}
|
||||
|
||||
private TestHttpClient(TestHttpClientHandler handler)
|
||||
: base(handler)
|
||||
{
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
public ConcurrentQueue<Entry> Requests => this.handler.Requests;
|
||||
|
||||
public Action<Entry> Handler
|
||||
{
|
||||
get => this.handler.Handler;
|
||||
set => this.handler.Handler = value;
|
||||
}
|
||||
|
||||
public class Entry
|
||||
{
|
||||
public Entry(HttpRequestMessage request)
|
||||
{
|
||||
Request = request;
|
||||
|
||||
Completion = new TaskCompletionSource<HttpResponseMessage>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
}
|
||||
|
||||
public TaskCompletionSource<HttpResponseMessage> Completion { get; }
|
||||
|
||||
public HttpRequestMessage Request { get; }
|
||||
|
||||
public bool IsGetStateRequest => Request.Method == HttpMethod.Get;
|
||||
|
||||
public bool IsSetStateRequest => Request.Method == HttpMethod.Post;
|
||||
|
||||
public void Respond(HttpResponseMessage response)
|
||||
{
|
||||
Completion.SetResult(response);
|
||||
}
|
||||
|
||||
public void RespondWithJson<TValue>(TValue value, JsonSerializerOptions options = null)
|
||||
{
|
||||
var bytes = JsonSerializer.SerializeToUtf8Bytes(value, options);
|
||||
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK);
|
||||
response.Content = new ByteArrayContent(bytes);
|
||||
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json") { CharSet = "UTF-8", };
|
||||
|
||||
Completion.SetResult(response);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestHttpClientHandler : HttpMessageHandler
|
||||
{
|
||||
public TestHttpClientHandler()
|
||||
{
|
||||
Requests = new ConcurrentQueue<Entry>();
|
||||
}
|
||||
|
||||
public ConcurrentQueue<Entry> Requests { get; }
|
||||
|
||||
public Action<Entry> Handler { get; set; }
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entry = new Entry(request);
|
||||
Handler?.Invoke(entry);
|
||||
Requests.Enqueue(entry);
|
||||
|
||||
using (cancellationToken.Register(() => entry.Completion.TrySetCanceled()))
|
||||
{
|
||||
return await entry.Completion.Task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue