Skip to content

Prototype of new API with the Graph and Downstream web API services #431

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

jmprieur
Copy link
Collaborator

@jmprieur jmprieur commented Aug 11, 2020

Proposal for Microsoft.Graph support

  1. Rename CallsWebApi in CallsWebApis, as this becomes a hub for all what calls a Web API. The alternative design would have been to have CallsMicrosoftGraph and CallsDownstreamApi under AddMicrosoftIdentityWebApp / WebApi, however, this would have obliged the developer =to pass-in the ConfidentialClientApplicationOptions to each of the services they want to call (that is several times if they call several services, for instance both Graph, and a DownstreamApi for ARM, and a Downstream for https://myWebApi.
    This is now possible having several sections in the appsettings.json. See for instance the Blazor sample which call graph and another API (Graph really, but that would not matter)
  2. Renamed CallsMicrosoftGraph into AddMicrosoftGraphServiceClient as GraphServiceClient this the type that the developer will use in the controller and is what they will look for in the documentation. This method is present under CallsWebApis, because otherwise we'd have to re-pass the initialization for CallsWebApis (the ConfidentialClientApplicationOptions)
  3. Updates the tests to get a feel of the API (In particular WebAppCallsGraph and BlazorServerCallsGraph

Proposal for Downstream web API support

  • Downstream API support* folder contains options, service, extensions
  • Aligning the properties for Microsoft Graph options and Downstream API options
  • Updated the Blazor sample to use both the Graph service and the Downstream API service (BlazorServer calls graph)

Todo list sample

  • changed the namespace of the model (common to client and service). Ideally we'd put in a shared project. We might do that later.
  • Updated the TodoListService class to leverage the DownstreamWebApi service. The code is way simpler!!!
  • updated the AppSettings.json and the Startup.cs to leverage these services
  • Fixed the Controller of the Web API which was not using the display name when adding an item (list always empty)

--------------------------------------------
1. Rename CallsWebApi in CallsWebApis
2. Rename CallsMicrosoftGraph into AddMicrosoftGraphServiceClient as this is what the developer will use in the controller, under CallsWebApis, because otherwise we'd have to re-pass the initialization for CallsWebApis (the ConfidentialClientApplicationOptions)
3. Updates the tests to get a feel of the API.
- Downstream API support folder contains options, service, extensions
- Aligning the properties for Microsoft Graph options and Downstream API options

Updated the Blazor sample to use both the Graph service and the Downstream API service (BlazorServer calls graph)
Updated the WebAppCallsGraph service

Todo list sample:
- changed the namespace of the model (common to client and service). Ideally we'd put in a shared project. We might do that later.
- Updated the TodoList service to leverage the DownstreamWebApi service. The code is way simpler!!!
- updated the AppSettings.json and the Startup.cs to leverage these services
- Fixed the Controller of the Web API which was not using the display name when adding an item.
@jmprieur jmprieur requested review from jennyf19 and pmaytak August 11, 2020 13:48
.CallsWebApi()
.CallsWebApis()
.AddMicrosoftGraphServiceClient()
.AddMicrosoftGraphServiceClient(Configuration.GetSection("GraphBeta"))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really one or the other of these 3 lines;

{
_todoListService = todoListService;
_todoListService = new TodoListService(downstreamWebApi);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too satisfied it, but I did not yet manage to inject the DownstreamApiService.
If we decided to go with this design, we'll have to fix that.

@@ -3,7 +3,7 @@

using System.Collections.Generic;
using System.Threading.Tasks;
using TodoListService.Models;
using TodoList.Models;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed the namespace because:

  • it's common to the service and client (it's even the same file with a link)
  • There is was a naming conflict between TodoListService (the namespace for this models), and the TodoListService class in the TodoListClient.

{
builder.AddDownstreamApiService(TodoListService.ServiceName, configuration);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just leverage the generic DownstreamApiService

}

throw new HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}.");
return await _downstreamWebApi.CallWebApiForUserAsync<Todo, Todo>(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use strongly typed wrappers and express just what is different. The code is way simpler and no copy/paste any longer.

@@ -23,7 +23,6 @@

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="3.1.1" />
<PackageReference Include="Microsoft.Graph" Version="1.16.0" />
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Microsoft.Graph is now included in Microsoft.Identity.Web. We don't need to reference it any longer in the samples

@@ -1,4 +1,4 @@
@model TodoListService.Models.Todo
@model TodoList.Models.Todo
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consequence of the renaming of namespace ..

@@ -76,7 +77,7 @@ public IActionResult Post([FromBody] Todo todo)
{
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
int id = TodoStore.Values.OrderByDescending(x => x.Id).FirstOrDefault().Id + 1;
Todo todonew = new Todo() { Id = id, Owner = HttpContext.User.Identity.Name, Title = todo.Title };
Todo todonew = new Todo() { Id = id, Owner = User.GetDisplayName(), Title = todo.Title };
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol! we forgot this one when we updated the GetAsync()

@@ -33,7 +33,7 @@ public void ConfigureServices(IServiceCollection services)
// Adds Microsoft Identity platform (AAD v2.0) support to protect this Api
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration, "AzureAd")
.CallsWebApi()
.CallsWebApis()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming to express that this is a hub for downstream APIs.
Note that CallsWebApis() really provides the ITokenAcquisitionService .... but I don't think we want to write that?

@@ -284,7 +284,7 @@ public async Task AddMicrosoftWebAppCallsWebApi_WithConfigActionParameters()

var builder = services.AddAuthentication()
.AddMicrosoftWebApp(_configureMsOptions, null, OidcScheme)
.CallsWebApi(_configureAppOptions, initialScopes);
.CallsWebApis(_configureAppOptions, initialScopes);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See, in this case, if we had chosen the other design, we'd have to pass-in the _configureAppOptions in all the CallsMicrosoftGraph and CallsDownstreamApis, with a risk of errors (pass-in different things, whereas this would be the same service, really), and duplication of code.

"ClientSecret": "secret-goes-here"
"ClientSecret": "fvHwfoLA8Zls6Huj5MBUQUDrJ5HXrauFQKDd2CacxJQ="
},
"GraphBeta": {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the case of a sample that calls several sevices:

  • Graph
  • And a downstream API (which turns out to be Graph v1.0, but could be something else.

.CallsWebApi(scopes)
.CallsWebApis()
.AddMicrosoftGraphServiceClient(Configuration.GetSection("GraphBeta"))
.AddDownstreamApiService("CalledApi", Configuration.GetSection("CalledApi"))
.AddInMemoryTokenCaches();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the kind of code that you would have if you want to call several services.

If you want to use ITokenAquisition directly, just use .CallsWebApis()
Otherwise:

  • if you want to use the MicrosoftGraphServiceClient wrapper, use .AddMicrosoftGraphServiceClient under .CallsWebApis()
  • if you want to use one or several instances of the downstream API service (they have a name, which here, I've chosen the same as the section in the config file to make it simpler, but that's not absolutely necessary), you woud have one or several calls to .AddDownstreamApiService("NameOfTheService", Configuration.GetSection("SectionInConfig"))

Then in the controller, you'd use IDownstreamWebApi.CallsWebApiForUser("NameOfTheService", and have the possiblity to override the configuration from the config file with a delegate.


protected override async Task OnInitializedAsync()
{
try
{
apiResult = await downstreamAPI.CallWebApiAsync("me");
response = await downstreamAPI.CallWebApiForUserAsync(
"CalledApi",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we call the service "CalledApi", on behalf of the user (this maps to some configuration for this service: see below), and we override the relative path to call the "me" endpoint. All the other configurations are overridable as well.

if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
apiResult = await response.Content.ReadAsStringAsync();
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we use the pattern recommended by Damien, but we could also use the strongly typed wrapper CallWebApiForUserAsync<InputType, ReturnType> which makes it even easier. It's used in the TodoListService wrapper in the TodoListClient project.


/// <inheritdoc/>
public async Task<HttpResponseMessage> CallWebApiForUserAsync(
string optionsInstanceName,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: should we stick w/consistency of the naming? MicrosoftIdentityCallWebApiForUserAysnc()....i don't like this much, but i think things should feel like they are part of MS identity web, or not...again, will need to think about it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of feel like I would leave this one as is.

My thought is that we should be careful adding prefixes in too many places since that can create a lot of noise (from my experience).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here this is a verb (method) on a class that we own, not an extension method on a class that we don't own (like Service Collection). I don't think it's abiguous. I would stick to CallWebApiForUserAsync in this particular case

@jmprieur jmprieur marked this pull request as draft August 12, 2020 11:01
@jmprieur jmprieur changed the base branch from jennyf/newAPI to jennyf/newApiPlusGraphService August 12, 2020 11:32
/// <summary>
/// Base URL for the called Web API. For instance <c>"https://graph.microsoft.com/beta/".</c>.
/// </summary>
public string BaseUrl { get; set; } = "https://graph.microsoft.com/v1.0";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a general options class, should the BaseUrl default be an empty string instead then?


/// <inheritdoc/>
public async Task<HttpResponseMessage> CallWebApiForUserAsync(
string optionsInstanceName,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of feel like I would leave this one as is.

My thought is that we should be careful adding prefixes in too many places since that can create a lot of noise (from my experience).

jsoncontent = new StringContent(jsonRequest, Encoding.UTF8, "application/json");
// case of patch? jsoncontent = new StringContent(jsonRequest, Encoding.UTF8, "application/json-patch+json");
}
else
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this else is unnecessary, jsoncontent would still be null here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried but the compiler was not happy line 131.

}

/// <inheritdoc/>
public async Task<HttpResponseMessage> CallWebApiForAppAsync(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we also want to have a generic typed version for this method?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more work.

/// <returns>The value returned by the API.</returns>
public Task<TOutput> CallWebApiForUserAsync<TInput, TOutput>(
string optionsInstanceName,
TInput input,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

input > requestContent?
Also maybe TInput, TOutput to TRequestContent, TResponseContent?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No sure about this one. The content is the json. Here input and output are object types (like Todo)

@jennyf19
Copy link
Collaborator

This can be closed, correct? @jmprieur you did two smaller PRs instead right?

@jmprieur
Copy link
Collaborator Author

No, there are the templates still

@jmprieur jmprieur closed this Aug 19, 2020
@jmprieur jmprieur deleted the jennyf/newAPIWithBuilderRefactoringToPrepareServices branch August 19, 2020 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants