Azure AD Graph client library batch processing - azure

Could some one tell me whether I can use the batch processing to add group memberships?
if yes can you please give me an example
thanks in advance
kind regards,
Snegha

According to the documentation, Graph API support batch processing. The Microsoft Azure Active Directory Client support batch processing.
You can find a lot of samples to use the Azure AD Graph API here :
Azure Samples for active directory
Especially you have a full sample on most of the actions that you can perform with the Graph API here :
Call the Azure AD Graph API from a native client (search for "Batch Operations" in the program.cs file).
Unfortunately batch processing is not working for navigation properties or at least I did not find a way to make it work...
So let's have a look at the documentation.
Graph API support for OData batch requests:
A query is a single query or function invocation.
A change set is a group of one or more insert, update, or delete operations, action invocations, or service invocations.
A batch is a container of operations, including one or more change sets and query operations.
The Graph API supports a subset of the functionality defined by the
OData specification:
A single batch can contain a maximum of five queries and/or change sets combined.
A change set can contain a maximum of one source object modification and up to 20 add-link and delete-link operations
combined. All operations in the change set must be on a single source
entity.
Here is the batch request syntax :
https://graph.windows.net/TenantName/$batch?api-version=1.6
A batch request is sent to the server with a single POST directive.
The payload is a multi-part MIME message containing the batch and its constituent queries and change sets. The payload includes two types of MIME boundaries:
A batch boundary separates each query and/or change set in the batch.
A change set boundary separates individual operations within a change set.
An individual request within a change set is identical to a request made when that operation is called by itself. (here is a sample request)
You can find here the full sample code : azure-active-directory-batchprocessing
So Basically, you need to obtain the authentication token:
var authority = "https://login.microsoftonline.com/mytenantName.onmicrosoft.com";
var resource = "https://graph.windows.net/";
var clientId = "ClientId of the application in the Azure Active Directory";
var clientSecret = "ClientSecret of the application in the Azure Active Directory";
var token = new AuthenticationContext(authority, false).AcquireToken(resource,
new ClientCredential(clientId, clientSecret)).AccessToken;
In your question you'd like to add member to a group (see Graph API Documentation on groups):
// Get the objectId of the group
var groupId = ...
// Get the member ids you'd like to add to the group
var memberIds = ...
Here is the code to add members to a group:
private static async Task AddMemberToGroup(string token, string groupId, IList<string> memberIds)
{
if (memberIds.Count > 100)
{
// A batch can contain up to 5 changesets. Each changeset can contain up to 20 operations.
throw new InvalidOperationException("Cannot send more than 100 operation in an batch");
}
var batch = new BatchRequest("https://graph.windows.net/MyTenantName.onmicrosoft.com");
// A changeset can contain up to 20 operations
var takeCount = 20;
var skipCount = 0;
var take = memberIds.Skip(skipCount).Take(takeCount).ToList();
while (take.Count > 0)
{
AddChangeset(batch, groupId, take);
skipCount += takeCount;
take = memberIds.Skip(skipCount).Take(takeCount).ToList();
}
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
var response = await client.SendAsync(batch.Request);
}
}
private static void AddChangeset(BatchRequest batch, string groupId, IEnumerable<string> memberIds)
{
var changeset = batch.AddChangeSet();
foreach (var memberId in memberIds)
{
// Create the HttpRequest to add a member to a group
var request = AddMemberToGroupRequest(groupId, memberId);
// Add the operation to the changeset
changeset.AddOperation(request);
}
}
private static HttpRequestMessage AddMemberToGroupRequest(string groupId, string memberId)
{
// Create a request to add a member to a group
var request = new HttpRequestMessage(HttpMethod.Post,
$"https://graph.windows.net/MyTenantName.onmicrosoft.com/groups/{groupId}/$links/members?api-version=1.6");
// Create the body of the request
var jsonBody =
JsonConvert.SerializeObject(new DirectoryObject($"https://graph.windows.net/MyTenantName.onmicrosoft.com/directoryObjects/{memberId}"));
// Set the content
request.Content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
// Return the request
return request;
}
public class BatchRequest
{
private readonly MultipartContent _batchContent;
public BatchRequest(string tenantUrl)
{
// Create the batch request
Request = new HttpRequestMessage(HttpMethod.Post,
$"{tenantUrl}/$batch?api-version=1.6");
// Initializes the batch content
_batchContent = new MultipartContent("mixed", "batch_" + Guid.NewGuid());
Request.Content = _batchContent;
}
public HttpRequestMessage Request { get; }
public ChangeSet AddChangeSet()
{
// Create a new changeset
var changeSet = new ChangeSet();
// Add the content of the changeset to the batch
_batchContent.Add(changeSet.Content);
// return the changeset
return changeSet;
}
public HttpMessageContent CreateOperation(HttpRequestMessage request)
{
var content = new HttpMessageContent(request);
content.Headers.ContentType = new MediaTypeHeaderValue("application/http");
content.Headers.Add("Content-Transfer-Encoding", "binary");
return content;
}
}
public class ChangeSet
{
public ChangeSet()
{
Content = new MultipartContent("mixed", "changeset_" + Guid.NewGuid());
}
public MultipartContent Content { get; }
public void AddOperation(HttpRequestMessage request)
{
var operationContent = new HttpMessageContent(request);
operationContent.Headers.ContentType = new MediaTypeHeaderValue("application/http");
operationContent.Headers.Add("Content-Transfer-Encoding", "binary");
Content.Add(operationContent);
}
}
public class DirectoryObject
{
public DirectoryObject(string url)
{
this.url = url;
}
public string url { get; }
}

Related

How to add customDimensions and set operation_parentId for Azure function log

I created a http trigger V1 azure function on net framework 4.8, and used ILogger for logging. The code is like this.
I checked the Application Insight and queried for traces table. This table contains columns named customDimensions and operation_ParentId. May I ask is there anyway to add custom property in customDimensions column, or set a new Guid value for operation_ParentId? I know that I can use TelemetryClient sdk to create a custom telemetry client for logging. Just curious if there is any easy way which doesn't need to create a new telemetry client, because azure function offers bulit-in integration with application insight.
Also, since azure function runtimes automatically tracks requests, is there any way to change the operation_ParentId and customDimensions for requests table as well? Thanks a lot!
To get both the headers and App Insights to get the custom operation Id, two things must be overridden.
The first is an Activity that wraps the HttpClient, which is responsible for controlling the correlation headers and the other is App Insights' dependency tracing.
Although you can disable Actions completely in your HttpClients, you can just remove the one in the client by setting Activity.Current = null to limit side effects.
var operationId = "CR" + Guid.NewGuid().ToString();
var url = "https://www.microsoft.com";
using (var client = new HttpClient())
{
using (var requestMessage =
new HttpRequestMessage(HttpMethod.Get, url))
{
//Makes the headers configurable
Activity.Current = null;
//set correlation header manually
requestMessage.Headers.Add("Request-Id", operationId);
await client.SendAsync(requestMessage);
}
}
The next step is to remove the App Insights default tracking for this request. Again, you can disable dependency tracking completely, or you can filter out the default telemetry for this request. Processors are registered inside the Startup class just like initializers.
services.AddApplicationInsightsTelemetryProcessor<CustomFilter>();
public class CustomFilter : ITelemetryProcessor
{
private ITelemetryProcessor Next { get; set; }
// next will point to the next TelemetryProcessor in the chain.
public CustomFilter(ITelemetryProcessor next)
{
this.Next = next;
}
public void Process(ITelemetry item)
{
// To filter out an item, return without calling the next processor.
if (!OKtoSend(item)) { return; }
this.Next.Process(item);
}
// Example: replace with your own criteria.
private bool OKtoSend(ITelemetry item)
{
var dependency = item as DependencyTelemetry;
if (dependency == null) return true;
if (dependency.Type == "Http"
&& dependency.Data.Contains("microsoft.com")
//This key is just there to help identify the custom tracking
&& !dependency.Context.GlobalProperties.ContainsKey("keep"))
{
return false;
}
return true;
}
}
Finally, you must inject a telemetry client and call TelemetryClient.TrackDependency() in the method that makes the remote call.
var operationId = "CR" + Guid.NewGuid().ToString();
//setup telemetry client
telemetry.Context.Operation.Id = operationId;
if (!telemetry.Context.GlobalProperties.ContainsKey("keep"))
{
telemetry.Context.GlobalProperties.Add("keep", "true");
}
var startTime = DateTime.UtcNow;
var timer = System.Diagnostics.Stopwatch.StartNew();
//continue setting up context if needed
var url = "https:microsoft.com";
using (var client = new HttpClient())
{
//Makes the headers configurable
Activity.Current = null;
using (var requestMessage =
new HttpRequestMessage(HttpMethod.Get, url))
{
//Makes the headers configurable
Activity.Current = null;
//set header manually
requestMessage.Headers.Add("Request-Id", operationId);
await client.SendAsync(requestMessage);
}
}
//send custom telemetry
telemetry.TrackDependency("Http", url, "myCall", startTime, timer.Elapsed, true);
Refer here more information.
Note: The above is possible by disabling the built-in dependency tracking and App Insights and handling it on your own. But the better approach is let .NET Core & App Insights do the tracking.

Unable to manual trigger my Azure Timer Trigger using httpclient post request

https://learn.microsoft.com/en-us/azure/azure-functions/functions-manually-run-non-http
I am trying to manual trigger my Azure Timer function App created in 2.0 and developed in .net core 2.0.
When I try to hit the url I get 403 error.
apikey I pass is picked from :
As the article you provided, you need to use _master key under Manage and Host key
I use the following class in my integration tests against service bus triggered Azure Functions.
class AzureFunctionCaller
{
private readonly HttpClient _httpClient;
private readonly string _functionUri;
public AzureFunctionCaller(string functionName)
{
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add("x-functions-key","<Key>");
_functionUri = $"<FUNCTION_ENDPOINT>/admin/functions/{functionName}";
}
public async Task CallViaAdminEndpoint(string content)
{
var httpContent = new StringContent(content, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(_functionUri, httpContent);
var responseContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Response content: {responseContent}");
}
}
Then you must send the data in a format where you place the content in "input" object.
var azureFunctionCaller = new AzureFunctionCaller("<FunctionName>");
var obj = new
{
... // properties you want to send
};
var jsonContent = JsonConvert.SerializeObject(new
{
input = JsonConvert.SerializeObject(obj)
});
await azureFunctionCaller.CallViaAdminEndpoint(jsonContent);`
To explain the input property, here is how the same call looks like in postman:

How can I fetch an array of data from a custom GI using soap contract based web services

I know how to fetch data from a custom Generic Inquiry using standard soap / page-based web services.
Here's my code for standard web services to get the results from a custom GI:
static void Main(string[] args)
{
GI000081.Screen context = new GI000081.Screen();
context.Url = "http://localhost/AcumaticaDB181000062/(W(6))/Soap/GI000081.asmx";
context.CookieContainer = new System.Net.CookieContainer();
LoginResult loginResult = context.Login("admin", "Passw0rd");
if (loginResult.Code != ErrorCode.OK)
{
throw new Exception(loginResult.Message);
}
GI000081.Content GI000081Content;
GI000081Content = context.GetSchema(); //.IN202500GetSchema();
//Here's the code to obtain the GI data:
string[][] GI000081Data = context.Export
(new Command[] {
GI000081Content.Result.AccountID,
GI000081Content.Result.Address,
GI000081Content.Result.CustomerID,
GI000081Content.Result.AccountName
},
null, //This is the filter - none here, so null..
0,
false,
false
);
}
My request is, can I get an example of C# code for how to do this using the Contract-based web services. I know how to extend the endpoint and get the wsdl file / service reference to my custom Generic Inquiry, but I don't know the syntax to make the actual call.
Thanks in advance...
Just to make sure that you create the entity in the endpoint properly, make sure that the top level entity contain only the Parameters and that it has a sub entity of type details contain all the results. If there is no parameter then it is fine for the top level entity to be empty.
Here is the code sample that I used
class Program
{
static void Main(string[] args)
{
DefaultSoapClient client = new DefaultSoapClient();
client.Login("admin", "admin", null, null, null);
try
{
BatchPaymentsInq batch = new BatchPaymentsInq
{
Result = new BatchPaymentsInqResult[]
{
new BatchPaymentsInqResult { ReturnBehavior = ReturnBehavior.All }
}
};
var result = client.Get(batch);
}
catch(Exception ex)
{
}
finally
{
client.Logout();
}
}
}
Edit:
Here is how I extended my endpoint in order to use it with the Contract Based SOAP API
So the main entity named BatchPaymentsInq is pointing to the Generic Inquiry screen and will not have any field in it as you have mentioned that there is no parameter.
The sub entity Result is an array of BatchPaymentsInqResult an object created for containing the fields in the result grid of the inquiry.

Azure failed to marshal transaction into propagation token for elastic transaction (Works for MSDTC)

In windows azure we have hosted two asp.net webapi project as app service. We need to enable distributed transaction here. We initiate transaction inside one api. Then inside that transaction scope we fetch propagation token of that transaction and send it as a header during another api call. The code is something like bellow.
[HttpGet]
[Route("api/Test/Transaction/Commit")]
public async Task<string> Commit()
{
using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions
{
IsolationLevel = IsolationLevel.ReadCommitted
},
TransactionScopeAsyncFlowOption.Enabled))
{
// cross app domain call
using (var client = new HttpClient())
{
using (var request = new HttpRequestMessage(HttpMethod.Get, ConfigurationManager.AppSettings["IdentityServerUri"] + "api/Test/Transaction/NoCommit"))
{
// forward transaction token
request.AddTransactionPropagationToken();
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
}
}
this.Repository.Insert(new Currency { Ccy = "x", IsoCode = "XIS", Name = "XYZ", CurrencyId = 9 });
await this.Repository.SaveChangesAsync();
scope.Complete();
return "value";
}
}
public static class HttpRequestMessageExtension
{
public static void AddTransactionPropagationToken(this HttpRequestMessage request)
{
if (Transaction.Current != null)
{
var token = TransactionInterop.GetTransmitterPropagationToken(Transaction.Current);
request.Headers.Add("TransactionToken", Convert.ToBase64String(token));
}
}
}
Inside the api(...api/Test/Transaction/NoCommit) to which we are making the call inside transaction scope, fetch that marshaled propagation token of the transaction from header and using it create instance of that transaction and instantiate TransactionScope using that transaction. Later we use this transaction scope to complete that transaction. We have introduced a action filter to apply this and added that filter to the action which is responsible for that api call. Code for that api and action filter is something like bellow.
[HttpGet]
[EnlistToDistributedTransactionActionFilter]
[Route("api/Test/Transaction/NoCommit")]
public async Task<string> NoCommit()
{
this.Repository.Insert(new Client
{
Name = "Test",
AllowedOrigin = "*",
Active = true,
ClientGuid = Guid.NewGuid(),
RefreshTokenLifeTime = 0,
ApplicationType = ApplicationTypes.JavaScript,
Secret = "ffff",
Id = "Test"
}
);
await this.Repository.SaveChangesAsync();
return "value";
}
public class EnlistToDistributedTransactionActionFilter : ActionFilterAttribute
{
private const string TransactionId = "TransactionToken";
/// <summary>
/// Retrieve a transaction propagation token, create a transaction scope and promote the current transaction to a distributed transaction.
/// </summary>
/// <param name="actionContext">The action context.</param>
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.Request.Headers.Contains(TransactionId))
{
var values = actionContext.Request.Headers.GetValues(TransactionId);
if (values != null && values.Any())
{
byte[] transactionToken = Convert.FromBase64String(values.FirstOrDefault());
var transaction = TransactionInterop.GetTransactionFromTransmitterPropagationToken(transactionToken);
var transactionScope = new TransactionScope(transaction, TransactionScopeAsyncFlowOption.Enabled);
actionContext.Request.Properties.Add(TransactionId, transactionScope);
}
}
}
/// <summary>
/// Rollback or commit transaction.
/// </summary>
/// <param name="actionExecutedContext">The action executed context.</param>
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
if (actionExecutedContext.Request.Properties.Keys.Contains(TransactionId))
{
var transactionScope = actionExecutedContext.Request.Properties[TransactionId] as TransactionScope;
if (transactionScope != null)
{
if (actionExecutedContext.Exception != null)
{
Transaction.Current.Rollback();
}
else
{
transactionScope.Complete();
}
transactionScope.Dispose();
actionExecutedContext.Request.Properties[TransactionId] = null;
}
}
}
}
So if any exception occurs during this call (api/Test/Transaction/Commit) inside that transaction scope (either in firt api or second api) all the database change done by the both api will be rolled back. This is working fine locally. As locally we get support of MSDTC. But in Azure there is no MSDTC support. In azure we get support from Elastic transaction. Because of this when we are trying to fetch propagation token of the transaction from the first server we are getting exception. So when we try to execute bellow code
var transaction = TransactionInterop.GetTransactionFromTransmitterPropagationToken(transactionToken);
We are getting exception with message "Value does not fall within the expected range". This post saying that this method would require promotion to MSDTC by System.Transactions, but for elastic transaction how we will make it work? For elastic transaction we need to marshal transaction into propagation token. How to do this? Looking for the solution.
Elastic Transactions are designed to allow transactions across Azure SQL Database and Azure SQL Managed Instance from a single .net application in Azure.
It is not built for distributing transactions across clients.
"Only client-coordinated transactions from a .NET application are supported"
https://learn.microsoft.com/en-us/azure/azure-sql/database/elastic-transactions-overview

Does the Azure Storage Table query entities really has number limitations?

From MSDN, it seems there's a limitation for the number of entities returned by the Query service:
A query against the Table service may return a maximum of 1,000 entities at one time and may execute for a maximum of five seconds.
But as I wrote a sample to show this issue, I didn't find any limitations for the number of returned entities, here is my key code:
public class DataProvider
{
public static string PartitionKey
{
get { return "PartitionKey"; }
}
public static IEnumerable<CustomerEntity> MoreThanThousandData()
{
var result = new List<CustomerEntity>();
for (int i = 0; i < 1200; i++)
{
result.Add(new CustomerEntity(PartitionKey, Guid.NewGuid().ToString())
{
Name = Guid.NewGuid().ToString(),
Age = new Random().Next(10, 70)
});
}
return result;
}
}
Insert 1200 entities to the table:
public class AfterOptimize
{
public void InsertDataToTable()
{
var cloudData = DataProvider.MoreThanThousandData();
Console.WriteLine("Plan to insert {0} entities to the table.", cloudData.Count());
InsertDataToTableInternal(AzureTableService.Table, cloudData);
}
private void InsertDataToTableInternal(CloudTable table, IEnumerable<ITableEntity> data)
{
var splitedData = data.Chunk(100);
Parallel.ForEach(splitedData, item =>
{
var batchInsertOperation = new TableBatchOperation();
foreach (var tableEntity in item)
{
batchInsertOperation.Add(TableOperation.Insert(tableEntity));
}
table.ExecuteBatch(batchInsertOperation);
});
}
}
Then, read from the table, the partition key are all the same here:
public void ReadCloudData()
{
InsertMoreThanOneThousandDataToTable();
var query =
new TableQuery<CustomerEntity>().Where(TableQuery.GenerateFilterCondition("PartitionKey",
QueryComparisons.Equal, DataProvider.PartitionKey));
var result = AzureTableService.Table.ExecuteQuery(query);
Console.WriteLine("Read {0} entities from table.", result.Count()); // output 1200
}
I only used the latest Azure storage .NET client API.
I'm not able to find a documentation link but ExecuteQuery method handles continuation token internally and will return all entities in a table. Thus the behavior you're seeing is correct.
If you run Fiddler when you are executing this code, you will notice multiple requests are sent to table service. First request would be without continuation token but in subsequent requests you will see NextPartitionKey and NextRowKey querystring parameters.

Resources