I have the following. It seems to not be waiting after the request has been sent out in the GetAllProjects. It waits for the response, then doesn't wait for the response content to be read in.
NOTE: using nuget package for ReadAsAsync (could have used a json object to deserialize however)
public class Project
{
public string? Id { get; set; }
public string? Name { get; set; }
public string? Url { get; set; }
}
public class Main
{
public static void Main(string[] args)
{
AzureClient ac = new AzureClient();
var projects = ac.GetAllProjects();
}
}
public class AzureClient
{
private readonly HttpClient _client;
private const string PAT = "myToken";
private const string API_VERSION = "api-version=5.0";
public AzureClient()
{
_client = new HttpClient()
{
BaseAddress = new Uri("some uri"),
Timeout = TimeSpan.FromSeconds(30)
};
// added media type and passed in auth token to _client. client returns 200 on requests
}
public async Task<ICollection<Project>> GetAllProjects()
{
var response = await _client.GetAsync("_apis/projects?{API_VERSION }");
var projects = await response.Content.ReadAsAsync<dynamic>();
return projects.value.ToObject<ICollection<Project>>();
}
}
You are missing an await here
var projects = await ac.GetAllProjects();
you will also need to make your Main method async
Related
So, I have been struggling with this for a while now, but I am getting a bit further each day. But now I am really stuck.
What I am trying to do:
I have an automation account with a runbook (PowerShell)
I want to create a schedule
Then I want to connect that schedule to the runbook so the runbook is executed at a specific date and time.
I want to achieve this through C# with the Management API of Azure. I found a few webpages that should give me all the information I needed, but it doesn't work.
The first one is this: https://learn.microsoft.com/en-us/rest/api/automation/schedule/create-or-update?tabs=HTTP
I get this to work and I can see the schedule after I ran the code.
The second one is this: https://learn.microsoft.com/en-us/rest/api/automation/job-schedule/create?tabs=HTTP
And here I get stuck. I get a 404 error when I execute the request and I have no idea what I am doing wrong.
The code below is far from perfect, but it's a POC and not the actual application. I want to make it work before I refactor.
This class has all the logic for the schedule:
public class ScheduleLogic
{
const string subscriptionId = "######";
const string resourceGroupName = "######";
const string automationAccountName = "######";
const string tenantId = "######";
const string clientId = "######";
const string clientSecret = "######";
const string resourceId = "https://management.azure.com/";
public string accessToken { get; set; }
// THIS ONE DOESN'T WORK!
public async Task ConnectScheduleToRunbook(RunbookSchedule runbookSchedule)
{
StringContent body = new(JsonSerializer.Serialize(runbookSchedule));
body.Headers.ContentType = new MediaTypeHeaderValue("application/json");
string url = $"https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Automation/automationAccounts/{automationAccountName}/jobSchedules/{runbookSchedule.Properties.Schedule.JobScheduleId}?api-version=2019-06-01";
await ExecuteRequest(RequestType.Put, url, body);
}
public async Task CreateSchedule(Schedule schedule)
{
if (string.IsNullOrEmpty(schedule.JobScheduleId))
throw new Exception("Job schedule ID is empty");
StringContent body = new(JsonSerializer.Serialize(schedule));
body.Headers.ContentType = new MediaTypeHeaderValue("application/json");
string url = $"https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Automation/automationAccounts/{automationAccountName}/schedules/{schedule.JobScheduleId}?api-version=2019-06-01";
await ExecuteRequest(RequestType.Put, url, body);
}
public async Task DeleteSchedule(string scheduleName)
{
string url = $"https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Automation/automationAccounts/{automationAccountName}/schedules/{scheduleName}?api-version=2019-06-01";
await ExecuteRequest(RequestType.Delete, url, null);
}
private async Task ExecuteRequest(RequestType requestType, string url, StringContent? body)
{
HttpResponseMessage response;
using (HttpClient client = new())
{
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {await GetAccessToken()}");
switch (requestType)
{
case RequestType.Put:
if (body == null)
throw new Exception("Body cannot be empty");
response = await client.PutAsync(url, body);
break;
case RequestType.Delete:
response = await client.DeleteAsync(url);
break;
default:
throw new Exception("Unknown request type");
}
if (!response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
throw new Exception(content);
}
}
}
private async Task<string> GetAccessToken()
{
if (!string.IsNullOrEmpty(accessToken))
return accessToken;
AuthenticationContext authContext = new("https://login.microsoftonline.com/" + tenantId);
ClientCredential clientCreds = new(clientId, clientSecret);
AuthenticationResult authResult = await authContext.AcquireTokenAsync(resourceId, clientCreds);
accessToken = authResult.AccessToken;
return accessToken;
}
}
Models:
public class RunbookSchedule
{
public RunbookScheduleProperties Properties { get; set; }
}
public class RunbookScheduleProperties
{
public Schedule Schedule { get; set; }
public Runbook Runbook { get; set; }
public object Parameters { get; set; }
}
public class Runbook
{
public string Name { get; set; }
}
public class Schedule
{
public string Name { get; set; }
[JsonIgnore]
public string JobScheduleId { get; set; }
[JsonPropertyName("properties")]
public ScheduleProperties ScheduleProperties { get; set; }
}
public class ScheduleProperties
{
public string Description { get; set; }
public DateTime StartTime { get; set; }
public DateTime ExpiryTime { get; set; }
public string Frequency { get; set; }
public object AdvancedSchedule => new { };
}
And the unit tests I use to test the logic:
public class ScheduleTests
{
private readonly string jobId = "d52004cc-b7ec-4b9b-99c1-3922492f6e1b-1";
private readonly string runbookName = "RunFunctionApp";
private readonly ScheduleLogic scheduleService;
public ScheduleTests()
{
scheduleService = new();
}
[Fact]
public async Task Should_CreateASchedule()
{
Schedule newScheduleSettings = new()
{
Name = jobId,
JobScheduleId = jobId,
ScheduleProperties = new()
{
Description = "Here is another example",
StartTime = new DateTime(2024, 03, 27, 9, 0, 0, 0),
ExpiryTime = new DateTime(2024, 03, 27, 10, 0, 0, 0),
Frequency = "OneTime"
}
};
await scheduleService.CreateSchedule(newScheduleSettings);
}
[Fact]
public async Task Should_DeleteSchedule()
{
await scheduleService.DeleteSchedule(jobId);
}
[Fact]
public async Task Should_ConnectScheduleAndRunbook()
{
await scheduleService.ConnectScheduleToRunbook(new RunbookSchedule
{
Properties = new()
{
Schedule = new() { Name = jobId, JobScheduleId = jobId },
Runbook = new() { Name = runbookName },
Parameters = new { id = 12 }
}
});
}
}
Somehow I think I am doing something stupid and the fix/answer is really simple.
I tried to mess around with the URL because I think there is a problem with that, not sure why... Just a feeling.
Google did show me some results, but those were for schedules of other services. Runbooks and automation don't seem to be very popular.
ChatGPT comes with older, obsolete solutions. Visual Studio gives a lot of green and even red lines when I try those suggestions.
In my ASP.NET Core-6 Web API, I am given a third party API to consume and then return the account details. I am using WebClient.
api:
https://api.thirdpartycompany.com:2233/UserAccount/api/AccountDetail?accountNumber=112123412
Headers:
X-GivenID:Given2211
X-GivenName:Givenyou
X-GivenPassword:Given#llcool
Then JSON Result is shown below:
{
"AccountName": "string",
"CurrentBalance": 0,
"AvailableBalance": 0,
"Currency": "string"
}
So far, I have done this:
BalanceEnquiryResponse:
public class BalanceEnquiryResponse
{
public string Response
{
get;
set;
}
public bool IsSuccessful
{
get;
set;
}
public List<BalanceList> AccountBalances
{
get;
set;
}
}
BalanceList:
public class BalanceList
{
public string AccountNumber
{
get;
set;
}
public decimal CurrentBalance
{
get;
set;
}
public decimal AvailableBalance
{
get;
set;
}
public string Currency
{
get;
set;
}
}
Then the service is shown below.
IDataService:
public interface IDataService
{
BalanceEnquiryResponse GetAccountBalance(string accountNo);
}
public class DataService : IDataService
{
private readonly ILogger<DataService> _logger;
private readonly HttpClient _myClient;
public DataService(ILogger<DataService> logger, HttpClient myClient)
{
_logger = logger;
_myClient = myClient;
PrepareAPIHeaders(); // Actually apply the headers!
}
private void PrepareAPIHeaders()
{
_myClient.DefaultRequestHeaders.Add("X-GivenID", "Given2211");
_myClient.DefaultRequestHeaders.Add("X-GivenName", "Givenyou");
_myClient.DefaultRequestHeaders.Add("X-GivenPassword", "Given#llcool");
_myClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json; charset=utf-8");
_myClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "application/json; charset=utf-8");
}
// If you want to use async API, you need to go async all the way.
// So make this Method async, too!
public async Task<BalanceEnquiryResponse> GetAccountBalance(string accountNo)
{
_logger.LogInformation("Accessing Own Account");
var url = $"https://api.thirdpartycompany.com:2233/UserAccount/api/AccountDetail?accountNumber={accountNo}";
var response = await _myClient.GetAsync(url);
// vv Get your payload out of the Http Response.
var responseResults = await response.Content.ReadAsAsync<BalanceEnquiryResponse>();
return responseResults;
}
}
I tested the third party api with the headers on POOSTMAN:
https://api.thirdpartycompany.com:2233/UserAccount/api/AccountDetail?accountNumber=112123412
and it gives me expected result. But from my code, when I tried to call GetAccountBalance from the code below and I supplied model.account_number:
public async Task<BaseResponse> FinalResult(RequestDto model)
{
var response = new BaseResponse();
try
{
//Check account Balance
var accBalance = _dataAccess.GetAccountBalance(model.account_number);
if (!accBalance.IsSuccessful)
{
response.response_code = "";
response.response_description = "Could not fetch account for subscriber";
return response;
}
}
}
I got this error in:
response.response_description = "Could not fetch account for subscriber";
What am I doing wrongly, especially in public class DataService and how do I resolve it?
Thanks.
It might just be that we talking about a GetCall but sometimes i had problems with headers which are set directly on the client layer. So i would try to set them on the request, instead of the client.
I wrote this without a editor so i could not test it.
But you should get the gist of what im trying to do
public async Task<BalanceEnquiryResponse> GetAccountBalance(string accountNo)
{
_logger.LogInformation("Accessing Own Account");
var url = $"https://api.thirdpartycompany.com:2233/UserAccount/api/AccountDetail?accountNumber={accountNo}";
using(var request = new HttpRequestMessage(HttpMethod.Get, url))
{
//add headers to the request not the client
PrepareAPIHeaders(request);
var result = await _myClient.SendAsync(request);
result.EnsureSuccessStatusCode();
/*Read response and parse it into an object and return*/
}
return null;
}
private void PrepareAPIHeaders(HttpRequestMessage request)
{
request.Headers.Add("X-GivenID", "Given2211");
/*Add other Headers*/
}
I'm fairly new to C# so go easy on me.
I have a class UserData.
public class UserData
{
public int UserId { get; set; }
public string Token { get; set; }
public string ConversationID { get; set; }
}
And instantiate it in my main class.
public class Main {
var userDataObjects = new UserData();
userDataObjects.UserId = 5;
userDataObjects.Token = "sampleXh123xczx";
userDataObjects.ConversationID = "2";
protected DialogsTurn _GetDialog;
public main(){
_GetDialog = new DiagsTurn();
}
public override async Task OnTurnAsync()
{
await _GetDialog.GetNextDialog();
}
}
public class DialogsTurn : Controller
{
private readonly HttpClient client = new HttpClient();
private const string BASE_URL = "https://sample.net";
var userDataObjects = new UserData();
public async Task GetNextDialog()
{
HttpRequestMessage request = new HttpRequestMessage(
HttpMethod.Get,
$"{BASE_URL}/api/Dialogs/GetNextDialog?Id={userDataObjects.UserId}");
request.Headers.Add("Accept", "application/json");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", userDataObjects.Token);
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<dynamic>(body).result;
UserDataExtractedDto.DialogTurnID = result.dialogId;
}
}
And once it was instantiated I need to call it from other classes.
Let's say in my DialogSTurn class I have a GetNextDialog method and I need the Token and UserId. I know I can that data as a parameter. But it's not the case for my application that I built.
Response is successful, i can view it in Visual Studio, but when i try to get returned data, its null.
This is API https://yoda-api.appspot.com/api/v1/yodish?text=I%20am%20yoda
And this is my code:
public class YodishModel
{
public string yodish { get; set; }
}
public class YodishResult
{
public YodishModel Result { get; set; }
}
public class YodishService : iService
{
public string GetText(string text)
{
Lazy<RestClient> client = new Lazy<RestClient>(() => new RestClient($"http://yoda-api.appspot.com/api/v1/yodish?text={text}"));
var request = new RestRequest();
var response = client.Value.Execute<YodishResult>(request);
if (response.IsSuccessful)
{
return response.Data.Result.yodish;
}
return null;
}
public string ToUrl(string text)
{
return HttpUtility.UrlEncode(text);
}
}
Response is successful, i can view the result, but Result is null (NullPointerException).
Also, is there a way to use parameters here instead of using string interpolation? 'text' is part of the URL which is officially not a paremeter.
In your case, you were deserializing using a mismatched object. This is what I did to fix it:
public class YodishModel
{
public string yodish { get; set; }
}
public class YodishService
{
public string GetText(string text)
{
Lazy<RestClient> client = new Lazy<RestClient>(() => new RestClient($"https://yoda-api.appspot.com/api/v1/"));
var request = new RestRequest($"yodish").AddQueryParameter("text", Uri.EscapeDataString(text), true);
var response = client.Value.Execute<YodishModel>(request);
if (response.IsSuccessful)
{
return Uri.UnescapeDataString(response.Data.yodish);
}
return null;
}
}
I also added the AddQueryParameter, as you mentioned.
I have been following some tutorials (e.g., https://learn.microsoft.com/en-us/aspnet/web-api/overview/advanced/calling-a-web-api-from-a-net-client) and trying to retrieve a list of jobs and print them out in my .Net console app.
The test data I am using is located at https://boards-api.greenhouse.io/v1/boards/vaulttec/jobs and supplied it to client.BaseAddress.
Since I am able to compile and run the tutorial succesfully, I simply used the same code and changed some of it to run the above test data (see below).
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace GreenhouseJobs
{
public class Job
{
public string Id { get; set; }
public string Title { get; set; }
public string Location { get; set; }
public DateTime LastUpdated { get; set; }
}
class GreenhouseJobsClient
{
static HttpClient client = new HttpClient();
static void ShowJob(Job job)
{
Console.WriteLine($"Id: {job.Id}\tTitle: " +
$"{job.Title}\tLocation: {job.Location}\tLast Updated: {job.LastUpdated}");
}
static async Task<Uri> CreateJobAsync(Job job)
{
HttpResponseMessage response = await client.PostAsJsonAsync(
"vaulttec/jobs", job);
response.EnsureSuccessStatusCode();
// return URI of the created resource.
return response.Headers.Location;
}
static async Task<Job> GetJobAsync(string path)
{
Job job = null;
HttpResponseMessage response = await client.GetAsync(path);
if (response.IsSuccessStatusCode)
{
job = await response.Content.ReadAsAsync<Job>();
}
return job;
}
//static async Task<Product> UpdateProductAsync(Product product)
//{
// HttpResponseMessage response = await client.PutAsJsonAsync(
// $"api/products/{product.Id}", product);
// response.EnsureSuccessStatusCode();
// // Deserialize the updated product from the response body.
// product = await response.Content.ReadAsAsync<Product>();
// return product;
//}
//static async Task<HttpStatusCode> DeleteProductAsync(string id)
//{
// HttpResponseMessage response = await client.DeleteAsync(
// $"api/products/{id}");
// return response.StatusCode;
//}
static void Main()
{
RunAsync().GetAwaiter().GetResult();
Console.ReadLine();
}
static async Task RunAsync()
{
// Update port # in the following line.
client.BaseAddress = new Uri("https://boards-api.greenhouse.io/v1/boards/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
try
{
// Create a new product
Job job = new Job
{
Id = "323232",
Title = "Test",
Location = "Test",
LastUpdated = DateTime.Now
};
var url = await CreateJobAsync(job);
Console.WriteLine($"Created at {url}");
// Get the product
job = await GetJobAsync(url.PathAndQuery);
ShowJob(job);
// Update the product
//Console.WriteLine("Updating price...");
//product.Price = 80;
//await UpdateProductAsync(product);
// Get the updated product
//product = await GetProductAsync(url.PathAndQuery);
//ShowProduct(product);
// Delete the product
//var statusCode = await DeleteProductAsync(product.Id);
//Console.WriteLine($"Deleted (HTTP Status = {(int)statusCode})");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.ReadLine();
}
}
}
The problem is, when I run the app, it does not return anything. Error message: "Response status code does not indicate success: 404 (Not Found)".
There are couple issues here
You are not just trying to retrieve but also create first. I am not sure if this api supports a POST or not, it probably doesn't, hence a 404
The Job entity is incorrect. Location needs to be an object itself and not a mere string if you need it deserialized correctly. Here is a working version of get call
https://pastebin.com/85NnAQY9
namespace ConsoleApp3
{
public class JobsJson
{
public List<Job> Jobs { get; set; }
}
public class Job
{
public string Id { get; set; }
public string Title { get; set; }
public Location Location { get; set; }
public DateTime LastUpdated { get; set; }
}
public class Location
{
public string Name { get; set; }
}
class GreenhouseJobsClient
{
static HttpClient client = new HttpClient();
static void ShowJobs(List<Job> jobs)
{
foreach (var job in jobs)
{
Console.WriteLine($"Id: {job.Id}\tTitle: " +
$"{job.Title}\tLocation: {job.Location}\tLast Updated: {job.LastUpdated}");
}
}
static async Task<List<Job>> GetJobAsync(string path)
{
var jobs = new List<Job>();
HttpResponseMessage response = await client.GetAsync(path);
if (response.IsSuccessStatusCode)
{
var stringResponse = await response.Content.ReadAsStringAsync();
var re = JsonConvert.DeserializeObject<JobsJson>(stringResponse);
jobs = re.Jobs;
}
return jobs;
}
static void Main()
{
RunAsync().GetAwaiter().GetResult();
Console.ReadLine();
}
static async Task RunAsync()
{
// Update port # in the following line.
client.BaseAddress = new Uri("https://boards-api.greenhouse.io/v1/boards/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
try
{
// Get the product
var jobs = await GetJobAsync("vaulttec/jobs");
ShowJobs(jobs);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.ReadLine();
}
}
}
The 404 is being returned by this line of code:
HttpResponseMessage response = await client.PostAsJsonAsync("vaulttec/jobs", job);
This is because the URL https://boards-api.greenhouse.io/v1/boards/vaulttec/jobs returns 404 when you try and make a POST request. Probably you need to be authorised to create jobs.
You can however make a GET request to this URL just fine.