YAML pipeline looks like below
parameters:
- name: parameter1
type: string
steps:
task: PowerShell#2
inputs:
targetType: 'inline'
script: |
# Write your PowerShell commands here.
Write-Host ${{ parameters.parameter1 }}
C# code to queue build is like
var build = new Build()
{
Definition = definition,
Project = project
};
var dict = new Dictionary<string, string> { { "parameter1", "parametervalue" } };
build.Parameters = JsonSerializer.Serialize(dict);
buildClient.QueueBuildAsync(build).Wait();
I get exception Could not queue the build because there were validation errors or warnings. A value for the 'parameter1' parameter must be provided.
Any idea to fix this issue would be helpful.
It looks that this is not possible at the moment to run pipeline with passing runtime parameters over C# SDK. You found workaround using REST API.
This is not an issue with SDK. They are developed in their own pace. So functionality which is available in REST API is not always available at the same moment in SDK. It could be available in the future, but for the moment if you want to run it programmatically you need to use REST API.
I managed to reverse engineer some client code which might help others. It may not be fully production-ready, but feel free to improve on it:
public class PipelineHttpClient : VssHttpClientBase
{
public PipelineHttpClient(Uri baseUrl, VssCredentials credentials)
: base(baseUrl, credentials)
{
}
public PipelineHttpClient(Uri baseUrl, VssCredentials credentials, VssHttpRequestSettings settings)
: base(baseUrl, credentials, settings)
{
}
public PipelineHttpClient(Uri baseUrl, VssCredentials credentials, params DelegatingHandler[] handlers)
: base(baseUrl, credentials, handlers)
{
}
public PipelineHttpClient(Uri baseUrl, VssCredentials credentials, VssHttpRequestSettings settings, params DelegatingHandler[] handlers)
: base(baseUrl, credentials, settings, handlers)
{
}
public PipelineHttpClient(Uri baseUrl, HttpMessageHandler pipeline, bool disposeHandler)
: base(baseUrl, pipeline, disposeHandler)
{
}
public async Task<PipelineRun> RunPipelineAsync(Guid project, int pipelineId, object parameters, string refName = "refs/heads/master")
{
var method = HttpMethod.Post;
var locationId = Guid.Parse("7859261e-d2e9-4a68-b820-a5d84cc5bb3d");
object routeValues = new
{
project,
pipelineId,
//pipelineVersion = ""
};
var version = new ApiResourceVersion("6.0");
// ensure that the refName is prefixed correctly.
refName = refName.StartsWith("refs/heads/", StringComparison.InvariantCultureIgnoreCase)
? refName
: $"refs/heads/{refName}";
var content = (HttpContent) new ObjectContent<object>(new
{
StagesToSkip = new object[0],
TemplateParameters = parameters,
Variables = new object(),
Resources = new
{
Repositories = new
{
Self = new
{
RefName = refName
}
}
}
},
new VssJsonMediaTypeFormatter(true));
var queryParameters = new Dictionary<string, string>();
return await SendAsync<PipelineRun>(method, locationId, routeValues, version, content, queryParameters, cancellationToken: CancellationToken.None);
}
}
public class Pipeline
{
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("revision")]
public int Revision { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("folder")]
public string Folder { get; set; }
}
public class Repository
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
}
public class Self
{
[JsonProperty("repository")]
public Repository Repository { get; set; }
[JsonProperty("refName")]
public string RefName { get; set; }
[JsonProperty("version")]
public string Version { get; set; }
}
public class Repositories
{
[JsonProperty("self")]
public Self Self { get; set; }
}
public class Resources
{
[JsonProperty("repositories")]
public Repositories Repositories { get; set; }
}
public class PipelineRun
{
[JsonProperty("pipeline")]
public Pipeline Pipeline { get; set; }
[JsonProperty("state")]
public string State { get; set; }
[JsonProperty("createdDate")]
public DateTime CreatedDate { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("resources")]
public Resources Resources { get; set; }
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}
Usage:
PipelineHttpClient client = new PipelineHttpClient(...);
var project = Guid.Parse("TODO");
var pipelineId = 1234;
// pipeline parameters:
var parameters = new {meaningOfLife = 42};
var result = await client.RunPipelineAsync(project, pipelineId, parameters, "refs/heads/feature/my-branch");
if (result.State == "inProgress")
{
// TODO the pipeline is running!
}
these using statements are required for the classes above:
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
Related
I am practicing with web api. My goal is to create a Get endpoint, which receive data from an external api, then return a different result. external api link: https://www.themealdb.com/api/json/v1/1/search.php?f=a, The external api data looks like:
{
"meals": [
{
"idMeal": "52768",
"strMeal": "Apple Frangipan Tart",
"strDrinkAlternate": null,
"strCategory": "Dessert",
.....
},
{
"idMeal": "52893",
"strMeal": "Apple & Blackberry Crumble",
....
}
]
}
I want my endpoint provide a different result like the following:
[
{
"idMeal": "52768",
"strMeal": "Apple Frangipan Tart",
"ingredients": ["Apple", "sugar"...]
},
{
"idMeal": "52893",
"strMeal": "Apple & Blackberry Crumble",
"ingredients": ["Apple", "sugar"...]
}
]
The following code is what I attempted so far, It's working, but the moment I changed property ingredient1 from public to private, that ingredient in list will become null, also, there are so many ingredients, some of them are null by default, I don't want to add them if they are null, how can I fix these two issues? Thanks a lot
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;
using RestSharp;
namespace testAPI.Controllers;
public class Content
{
[JsonPropertyName("meals")]
public List<Meal> Meals { get; set; }
}
public class Meal
{
[JsonPropertyName("idMeal")]
public string MealId { get; set; }
[JsonPropertyName("strMeal")]
public string Name { get; set; }
[JsonPropertyName("strIngredient1")]
public string Ingredient1 { get; set; }
[JsonPropertyName("strIngredient2")]
public string Ingredient2 { get; set; }
[JsonPropertyName("strIngredient20")]
public string Ingredient20 { get; set; }
public List<string> Ingredients
{
get { return new List<string>(){Ingredient1, Ingredient2, Ingredient20};}
}
}
[ApiController]
[Route("api/[controller]")]
public class DishesController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAllRecipes()
{
var client = new RestClient($"https://www.themealdb.com/api/json/v1/1/search.php?s=");
var request = new RestRequest();
var response = await client.ExecuteAsync(request);
var mealList = JsonSerializer.Deserialize<Content>(response.Content);
return Ok(mealList.Meals);
}
}
To address the problems one at a time...
the moment I changed property ingredient1 from public to private, that ingredient in list will become null
Changing the access modifier affects both deserialization and serialization, so this cannot be used to only stop it from serializing the property. You should split the data models up into what you want to receive and what you want to expose/return.
there are so many ingredients, some of them are null by default, I don't want to add them if they are null
Addition to splitting up the data models you can handle this when mapping from one model to the other.
The following code should fix both issues:
namespace TheMealDb.Models
{
// These are the models you receive from TheMealDb
// JSON converted to classes with https://json2csharp.com/
public class Root
{
public List<Meal> meals { get; set; }
}
public class Meal
{
public string idMeal { get; set; }
public string strMeal { get; set; }
public string strIngredient1 { get; set; }
public string strIngredient2 { get; set; }
public string strIngredient3 { get; set; }
// Other properties removed for brevity...
}
}
namespace Internal.Models
{
// This is the model you want to return from your controller action
public class Meal
{
[JsonPropertyName("id")] // No need to use the same name as from themealdb
public string Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("ingredients")]
public List<string> Ingredients { get; set; }
}
}
Now, to fetch, map and return the data in your controller action:
[HttpGet]
public async Task<IActionResult> GetAllRecipes()
{
var client = new RestClient($"https://www.themealdb.com/api/json/v1/1/search.php?s=");
var request = new RestRequest();
var response = await client.ExecuteAsync(request);
// Deserialize to the "TheMealDb" models
var mealList = JsonSerializer.Deserialize<TheMealDb.Models.Root>(response.Content);
// Map to your own models
var myMealList = mealDbList.meals?.Select(MapToInternal);
return Ok(myMealList);
}
// Map "TheMealDb" model to your own model
private Internal.Models.Meal MapToInternal(TheMealDb.Models.Meal externalMeal)
{
return new Internal.Models.Meal
{
Id = externalMeal.idMeal,
Name = externalMeal.strMeal,
Ingredients = new []
{
externalMeal.strIngredient1,
externalMeal.strIngredient2,
externalMeal.strIngredient3,
// ...
}
// Remove empty/null ingredients
.Where(ingr => !string.IsNullOrEmpty(ingr))
.ToList()
};
}
See the code in action.
I am trying to use ServiceStack's AutoQuery with a service source, but am either unable to get caching working correctly, or have misunderstood how it is supposed to work.
What I am trying to achieve to to add query functionality to an 'edge' microservice which calls an internal service that serves up a complete list of data.
Minimal code to reproduce my problem:
class Program
{
static async Task Main(string[] args)
{
IWebHost host = new WebHostBuilder()
.UseKestrel((builderContext, options) => options.Configure(builderContext.Configuration.GetSection("Kestrel")))
.UseStartup<Startup>()
.Build();
await host.RunAsync();
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services) {}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseServiceStack(new AppHost());
app.Run(context => Task.FromResult(0));
}
}
public class AppHost : AppHostBase
{
public AppHost() : base("Hello Web Services", typeof(HelloService).Assembly){ }
public override void Configure(Funq.Container container)
{
container.AddSingleton<ICacheClient, MemoryCacheClient>(); // Otherwise HostContext.Cache is null
Plugins.Add(new AutoQueryDataFeature { MaxLimit = 3, IncludeTotal = true }.AddDataSource(ctx => ctx.ServiceSource<string>(new Hello(), HostContext.Cache, TimeSpan.FromMinutes(5))));
}
}
// Request DTO
[Route("/hello")]
[Route("/hello/{Name}")]
public class Hello : QueryData<NameDto>
{
[QueryDataField(Condition = "StartsWith", Field = nameof(Name))]
public string Name { get; set; }
}
public class NameDto
{
public string Name { get; set; }
}
public class HelloService : Service
{
public IAutoQueryData AutoQuery { get; set; }
public async Task<object> Any(Hello query)
{
//Imagine I was making a service call to another microservice here...
var data = new List<NameDto> { new NameDto { Name = "Bob" }, new NameDto { Name = "George" }, new NameDto { Name = "Baldrick" }, new NameDto { Name = "Nursey" }, new NameDto { Name = "Melchett" }, new NameDto { Name = "Kate" } };
DataQuery<NameDto> dataQuery = AutoQuery.CreateQuery(query, Request, new MemoryDataSource<NameDto>(data, query, Request));
return AutoQuery.Execute(query, dataQuery);
}
}
Nuget packages: Mircosoft.AspNetCore.All (2.2.1) and ServiceStack (5.4.0)
So, in a console (.NET Core 2.2), the above code will spin up and listen on port 5000.
If I query, I get my list, which is limited to the number of results as expected, and I can also skip / take as expected.
However, every time I invoke the service method, the results are not cached (which is specified when I registered the plug-in - cache for 5 minutes) and if I put a breakpoint in the service method, the list of 'Names' is re-created every time. This happens even if I make the same request to the service.
I'd like to be able to cache the result set (in memory is fine) and only hit the service method when the cache expires. What am I doing wrong (or misunderstanding) here?
Edit
Code that I used to try out Mythz suggestion... now I don't get any autoquery features working at all.
class Program
{
static async Task Main(string[] args)
{
IWebHost host = new WebHostBuilder()
.UseKestrel((builderContext, options) => options.Configure(builderContext.Configuration.GetSection("Kestrel")))
.UseStartup<Startup>()
.Build();
await host.RunAsync();
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseServiceStack(new AppHost());
app.Run(context => Task.FromResult(0));
}
}
public class AppHost : AppHostBase
{
public AppHost() : base("Hello Web Services", typeof(HelloService).Assembly){ }
public override void Configure(Funq.Container container)
{
container.AddSingleton<ICacheClient, MemoryCacheClient>();
Plugins.Add(new AutoQueryDataFeature { MaxLimit = 5 }
.AddDataSource(ctx => ctx.ServiceSource<GithubRepo>(ctx.Dto.ConvertTo<QueryGithubRepo>(),
HostContext.Cache, TimeSpan.FromMinutes(5))));
}
}
public class QueryGithubRepo : QueryData<GithubRepo>
{
public string User { get; set; }
public string Organization { get; set; }
}
public class GithubRepo
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Homepage { get; set; }
public int Watchers_Count { get; set; }
public int Stargazers_Count { get; set; }
public int Size { get; set; }
public string Full_Name { get; set; }
public DateTime Created_at { get; set; }
public DateTime? Updated_At { get; set; }
public bool Has_Downloads { get; set; }
public bool Fork { get; set; }
public string Url { get; set; } // https://api.github.com/repos/NetCoreWebApps/bare
public string Html_Url { get; set; }
public bool Private { get; set; }
public GithubRepo Parent { get; set; } // only on single result, e.g: /repos/NetCoreWebApps/bare
}
public class NameDto
{
public string Name { get; set; }
}
public class HelloService : Service
{
public object Get(QueryGithubRepo request)
{
if (request.User == null && request.Organization == null)
throw new ArgumentNullException("User");
var url = request.User != null
? $"https://api.github.com/users/{request.User}/repos"
: $"https://api.github.com/orgs/{request.Organization}/repos";
return url.GetJsonFromUrl(requestFilter: req => req.UserAgent = GetType().Name)
.FromJson<List<GithubRepo>>();
}
}
If you're using AutoQuery in your Service implementation that's just a Custom AutoQuery implementation not a AutoQuery Service Data Source which queries the results of a normal Service.
In this case it sounds like you do want a cacheable Auto Query Service Data Source which the docs shows an example of in its GetGithubRepos Service which makes an API call to GitHub's 3rd Party API:
public class QueryGithubRepo : QueryData<GithubRepo>
{
public string User { get; set; }
public string Organization { get; set; }
}
public object Get(GetGithubRepos request)
{
if (request.User == null && request.Organization == null)
throw new ArgumentNullException("User");
var url = request.User != null
? $"https://api.github.com/users/{request.User}/repos"
: $"https://api.github.com/orgs/{request.Organization}/repos";
return url.GetJsonFromUrl(requestFilter:req => req.UserAgent = GetType().Name)
.FromJson<List<GithubRepo>>();
}
Then you register it is a cached Service Data Source when registering the Service DataSource:
Plugins.Add(new AutoQueryDataFeature { MaxLimit = 100 }
.AddDataSource(ctx => ctx.ServiceSource<GithubRepo>(ctx.Dto.ConvertTo<GetGithubRepos>(),
HostContext.Cache, TimeSpan.FromMinutes(5)));
);
You can use HostContext.LocalCache to cache it in the local Memory Cache, instead of the registered ICacheClient caching provider.
for whatever reason I can't seem to figure out how to pull my object out of my queue and deserialize it back into what it was placed into it as (An AccountEventDTO).
Azure function successfully placing object into queue:
[FunctionName("AccountCreatedHook")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequestMessage req,
TraceWriter log, [ServiceBus("topic-name", Connection = "BusConnectionString", EntityType = Microsoft.Azure.WebJobs.ServiceBus.EntityType.Topic)] IAsyncCollector<BrokeredMessage> accountCreatedTopic)
{
var accountEvent = await req.Content.ReadAsAsync<AccountEventDTO>();
if (accountEvent != null && accountEvent.Name != null)
{
// Serialization
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(accountEvent));
var memoryStream = new MemoryStream(bytes, writable: false);
var message = new BrokeredMessage(memoryStream) { SessionId = Guid.NewGuid().ToString() };
await accountCreatedTopic.AddAsync(message);
return req.CreateResponse(HttpStatusCode.OK, "Account successfully added to topic.");
}
return req.CreateResponse(HttpStatusCode.BadRequest, "Account was not formed well.");
}
Azure function pulling object from queue:
[FunctionName("AccountCreatedSubscriber")]
public static void Run([ServiceBusTrigger("topic-name", "license-keys", Connection = "BusConnectionString")]BrokeredMessage accountEvent, ILogger log)
{
// ERROR on this line during deserialization
var account = accountEvent.GetBody<AccountEventDTO>();
var accountAddedEvent = Mapper.Map<AccountEventDTO, AccountAddedEvent>(account);
_accountHandler.Handle(accountAddedEvent);
GenericLogger.AccountLogging(log, accountAddedEvent);
}
Error message:
AccountEventDTO:
public class AccountEventDTO : IAccountEvent
{
public string Name { get; set; }
public string SugarId { get; set; }
public string AccountSubTypeRaw { get; set; }
public AccountType AccountType { get; set; } = AccountType.Customer;
public AccountSubType? AccountSubType { get; set; } = null;
public string Phone { get; set; }
public string PhoneAlternate { get; set; }
public string BillingAddressCity { get; set; }
public string BillingAddressCountry { get; set; }
public string BillingAddressPostalCode { get; set; }
public string BillingAddressState { get; set; }
public string BillingAddressStreet { get; set; }
public string ShippingAddressCity { get; set; }
public string ShippingAddressCountry { get; set; }
public string ShippingAddressPostalCode { get; set; }
public string ShippingAddressState { get; set; }
public string ShippingAddressStreet { get; set; }
public string Website { get; set; }
}
You're using BrokeredMessage (old Azure Service Bus client for .NET, WindowsAzure.ServiceBus). When message is sent as memory stream, it has to be received and deserialized using the same approach. GetBody<T> will work if you construct BrokeredMessage passing in an object of type T.
Note: the next generation client (Microsoft.Azure.ServiceBus) only works raw with byte array (memory stream for the old client). If this is a new project, recommend to stick with that approach rather than serialized types. More info is available in a GitHub issue here.
Ended up solving this by altering the way I was serializing my message on the send side and how I was pulling it down on the receiving side.
Sending Serialization:
var jsonString = JsonConvert.SerializeObject(accountEvent);
var message = new BrokeredMessage(jsonString);
message.SessionId = Guid.NewGuid().ToString();
message.ContentType = "application/json";
Receiving Deserialization:
var content = accountEvent.GetBody<string>();
var account = JsonConvert.DeserializeObject<AccountEventDTO>(content);
I had a question with regards to custom authorization for AWS API Gateway using a lambda coded in C#. In the documentation for AWS Lambdas, the function signature is as follows:
returnType handler-name(inputType input, ILambdaContext context) {
...
}
The inputType and returnType need to be specified for the function handler. For custom authorization in API Gateway, what should the inputType and returnTypes be? Thanks in advance.
You can opt for a strongly-typed approach without inventing custom classes that need to follow the required schema.
Use Nuget package:
Amazon.Lambda.APIGatewayEvents
Input schema:
https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-input.html
Output schema:
https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html
Your function prototype can then resemble:
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
public class Function
{
public APIGatewayCustomAuthorizerResponse FunctionHandler(APIGatewayCustomAuthorizerRequest input, ILambdaContext context)
{
bool ok = false;
// authorization logic here...
if(input.AuthorizationToken == "up-down-left-right-a-b-select-start")
{
ok = true;
}
return new APIGatewayCustomAuthorizerResponse
{
PrincipalID = "***",//principal info here...
UsageIdentifierKey = "***",//usage identifier here (optional)
PolicyDocument = new APIGatewayCustomAuthorizerPolicy
{
Version = "2012-10-17",
Statement = new List<APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement>() {
new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement
{
Action = new HashSet<string>(){"execute-api:Invoke"},
Effect = ok ? "Allow" : "Deny",
Resource = new HashSet<string>(){ "***" } // resource arn here
}
},
}
};
}
}
I thought I would elaborate this a bit. This uses part of what was done here as well as tried to make it like the example they give us here.
http://docs.aws.amazon.com/apigateway/latest/developerguide/use-custom-authorizer.html
I am not sure if it needs to be async or not? I didn't and this seemed to work pretty well for a basic start.
public class Authorize
{
public Authorize() { }
public AuthPolicy AuthorizeHandler(TokenAuthorizerContext request, ILambdaContext context)
{
var token = request.AuthorizationToken;
switch (token.ToLower())
{
case "allow":
return generatePolicy("user", "Allow", request.MethodArn);
}
return null;
}
private AuthPolicy generatePolicy(string principalId, string effect, string resource)
{
AuthPolicy authResponse = new AuthPolicy();
authResponse.policyDocument = new PolicyDocument();
authResponse.policyDocument.Version = "2012-10-17";// default version
authResponse.policyDocument.Statement = new Statement[1];
Statement statementOne = new Statement();
statementOne.Action = "execute-api:Invoke"; // default action
statementOne.Effect = effect;
statementOne.Resource = resource;
authResponse.policyDocument.Statement[0] = statementOne;
return authResponse;
}
}
public class TokenAuthorizerContext
{
public string Type { get; set; }
public string AuthorizationToken { get; set; }
public string MethodArn { get; set; }
}
public class AuthPolicy
{
public PolicyDocument policyDocument { get; set; }
public string principalId { get; set; }
}
public class PolicyDocument
{
public string Version { get; set; }
public Statement[] Statement { get; set; }
}
public class Statement
{
public string Action { get; set; }
public string Effect { get; set; }
public string Resource { get; set; }
}
I wanted to post the solution that I used that worked for me. Thanks to Josh Maag for pointing me in the right direction. Basically, I created a few simple classes:
public class TokenAuthorizerContext
{
public string Type { get; set; }
public string AuthorizationToken { get; set; }
public string MethodArn { get; set; }
}
public class AuthPolicy
{
public PolicyDocument policyDocument { get; set; }
public string principalId { get; set; }
}
public class PolicyDocument
{
public string Version { get; set; }
public Statement[] Statement { get; set; }
}
public class Statement
{
public string Action { get; set; }
public string Effect { get; set; }
public string Resource { get; set; }
}
```
With the above classes created, the signature to my handler is:
public async Task<AuthPolicy> FunctionHandler(TokenAuthorizerContext request, ILambdaContext context)
You should really take a look at the following link and try to follow it through. The full tutorial is written using Python, so if you're unfamiliar with it, just do your best to follow along and read the full walk-through, but this link will explain the C# portion:
http://docs.aws.amazon.com/lambda/latest/dg/get-started-step5-optional.html
Essentially, the string:
returnType handler-name(inputType input, ILambdaContext context) {
Would be something like this (copied from the AWS page):
public string MyHandler(int count, ILambdaContext context) { ... }
public is added as a scope modifier, the returnType the developer has chosen is string the handler-name is MyHandler and the inputType is int
This is my first question on SO, please let me know if I am doing anything wrong!
I am trying to parse an XML similar to this:
<LiveUpdate>
<CityID>F0A21EA2</CityID>
<CityName>CityTown</CityName>
<UserName>john</UserName>
<ApplicationDetails>
<ApplicationDetail
Application="AC"
Licensed="true"
Version="2015.2"
Patch="0001"
/>
<ApplicationDetail
Application="AP"
Licensed="true"
Version="2015.2"
Patch="0002"
/>
</ApplicationDetails>
</LiveUpdate>
I have classes that look like this:
public class Client
{
public string cityID { get; set; }
public string cityName { get; set; }
public string userName { get; set; }
public List<Apps> appList { get; set; }
}
public class Apps
{
public string app { get; set; }
public string licensed { get; set; }
public string version { get; set; }
public string patch { get; set; }
}
I need to be able to have a client class with a list of all the application details to be iterated over.
So far the best I've come up with is:
XDocument xml = XDocument.Load(#"C:\blah\Desktop\1.xml");
var liveUpdate = xml.Root;
var clients = (from e in liveUpdate.Elements()
select new Client()
{
cityID = e.Element("CityID").Value,
cityName = e.Element("CityName").Value,
userName = e.Element("UserName").Value,
appList = e.Elements("ApplicationDetails")
.Select(a => new Apps()
{
app = a.Element("Application").Value,
licensed = a.Element("Licensed").Value,
version = a.Element("Version").Value,
patch = a.Element("Patch").Value
}).ToList()
});
However, I'm currently running into an error that says Object reference not set to an instance of an object.
I've seen some similar examples on here, but not that deal with data before the multiple children.
I'm fairly new to XML and Linq so any help here would be greatly appreciated!
Your XML only contains one LiveUpdate tag, so rather than iterating over all of the elements inside of it, you just want to look at the Root element.
In ApplicationDetails, Application, Licensed and such are attributes, not elements. Use .Attribute() to access them.
ApplicationDetails is a single tag, and inside it you have ApplicationDetail tags.
There is no DateTime element in your LiveUpdate tag.
This works:
var liveUpdate = xml.Root;
var e = liveUpdate;
var clients = new Client()
{
cityID = e.Element("CityID").Value,
cityName = e.Element("CityName").Value,
userName = e.Element("UserName").Value,
//dateTime = e.Element("DateTime").Value,
appList = e.Element("ApplicationDetails").Elements("ApplicationDetail")
.Select(a => new Apps()
{
app = a.Attribute("Application").Value,
licensed = a.Attribute("Licensed").Value,
version = a.Attribute("Version").Value,
patch = a.Attribute("Patch").Value
}).ToList()
};
Since you have already defined a class into which you wish to deserialize, you can use XmlSerializer to deserialize it for you.
First, let's rename some of your property names to more closely match the XML and c# naming conventions:
[XmlRoot("LiveUpdate")]
public class Client
{
public string CityID { get; set; }
public string CityName { get; set; }
public string UserName { get; set; }
[XmlArray("ApplicationDetails")]
[XmlArrayItem("ApplicationDetail")]
public List<Apps> AppList { get; set; }
}
public class Apps
{
[XmlAttribute]
public string Application { get; set; }
[XmlAttribute]
public bool Licensed { get; set; }
[XmlAttribute]
public string Version { get; set; }
[XmlAttribute]
public string Patch { get; set; }
}
Then add the following extension methods:
public static class XmlSerializationHelper
{
public static T LoadFromXML<T>(this string xmlString)
{
using (StringReader reader = new StringReader(xmlString))
{
object result = new XmlSerializer(typeof(T)).Deserialize(reader);
if (result is T)
{
return (T)result;
}
}
return default(T);
}
public static T LoadFromFile<T>(string filename)
{
using (var fs = new FileStream(filename, FileMode.Open))
{
object result = new XmlSerializer(typeof(T)).Deserialize(fs);
if (result is T)
{
return (T)result;
}
}
return default(T);
}
}
Now you can deserialize from your XML file as follows:
string fileName = #"C:\blah\Desktop\1.xml";
var client = XmlSerializationHelper.LoadFromFile<Client>(fileName);
I manually updated your Client class to map correctly to the provided XML, but if you wanted to do it automatically, see here: Generate C# class from XML.