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:
Ryan Nowak 2019-10-04 22:11:59 -07:00 committed by Aman Bhardwaj
parent 447c93c8c2
commit 0b08737cb6
66 changed files with 2673 additions and 30 deletions

3
.gitignore vendored
View File

@ -75,3 +75,6 @@ bld/
*.vsp
*.vspx
/drop
# VS Code
.vscode/

View File

@ -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')))

View File

@ -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

View File

@ -0,0 +1,9 @@
namespace ControllerSample
{
public class Account
{
public string Id { get; set; }
public decimal Balance { get; set; }
}
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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>();
});
}
}

View File

@ -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"
}
}
}
}

View File

@ -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<>`.

View File

@ -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();
});
}
}
}

View File

@ -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; }
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View File

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

View File

@ -0,0 +1,7 @@
<Project>
<Import Project="$(MSBuildThisFileDirectory)..\properties\dapr_managed_netcore.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,9 @@
namespace RoutingSample
{
public class Account
{
public string Id { get; set; }
public decimal Balance { get; set; }
}
}

View File

@ -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>();
});
}
}

View File

@ -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"
}
}
}
}

View File

@ -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.

View File

@ -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>

View File

@ -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);
}
}
}
}

View File

@ -0,0 +1,9 @@
namespace RoutingSample
{
public class Transaction
{
public string Id { get; set; }
public decimal Amount { get; set; }
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View File

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

25
src/Directory.Build.props Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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" />

View File

@ -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;
}
}
}

View File

@ -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>());
});
}
}
}

View File

@ -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
{
}
}
}

View File

@ -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
{
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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>

View File

@ -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")]

View File

@ -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<>);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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>

View File

@ -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";
}
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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>

View File

@ -0,0 +1,7 @@
<Project>
<Import Project="$(MSBuildThisFileDirectory)..\properties\dapr_managed_netcore.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>
</Project>

View File

@ -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>

View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -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>();
});
}
}

View File

@ -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"
}
}
}
}

View File

@ -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();
});
});
}
}
}

View File

@ -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; }
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View File

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

View File

@ -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);
});
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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");
}
}
}
}
}

View File

@ -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>

View File

@ -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; }
}
}
}

View File

@ -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>

View File

@ -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; }
}
}
}

View File

@ -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);
}
}
}
}
}