Adding a custom query backed Navigation Property to ODataConventionModelBuilder - c#

Situation
I created the following Model classes
public class Car
{
public int Id {get;set;}
public string Name {get;set;}
public virtual ICollection<PartState> PartStates {get;set; }
}
public class PartState
{
public int Id {get;set;}
public string State {get;set;}
public int CarId {get;set;}
public virtual Car Car {get;set;}
public int PartId {get;set;}
public virtual Part Part {get;set;}
}
public class Part
{
public int Id {get;set;}
public string Name {get;set;}
}
And a matching DbContext
public class CarContext : DbContext
{
public DbSet<Car> Cars {get;set;}
public DbSet<PartState> PartStates {get;set;}
public DbSet<Part> Parts {get;set;}
}
And created a WebApplication to make this available via odata, using the scaffolding template "Web API 2 OData Controller with Actions, using Entity Framework"
also i create following webapi config:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Car>("Cars");
builder.EntitySet<PartState>("PartStates");
builder.EntitySet<Part>("Parts");
var edmModel = builder.GetEdmModel();
config.Routes.MapODataRoute("odata", "odata", edmModel);
}
}
I now want to add the following Method to my Cars Controller
// GET: odata/Cars(5)/Parts
[Queryable]
public IQueryable<Part> GetParts([FromODataUri] int key)
{
var parts = db.PartStates.Where(s => s.CarId == key).Select(s => s.Part).Distinct();
return parts;
}
And retrieve the data with this Url:
http://localhost/odata/Cars(1)/Parts
But it does not work, instead i get the following error:
{
"odata.error":{
"code":"","message":{
"lang":"en-US","value":"No HTTP resource was found that matches the request URI 'http://localhost/odata/Cars(1)/Parts'."
},"innererror":{
"message":"No routing convention was found to select an action for the OData path with template '~/entityset/key/unresolved'.","type":"","stacktrace":""
}
}
}
Question
So my question is, is that even possible?!
I tried to create a Navigation property manually, and added it to the edm model, while this does resolve the issue to invoke the new method, it also introduces new Errors.
EDIT:
What id did try to add it manually in this way:
var edmModel = (EdmModel)builder.GetEdmModel();
var carType = (EdmEntityType)edmModel.FindDeclaredType("Car");
var partType = (EdmEntityType)edmModel.FindDeclaredType("Part");
var partsProperty = new EdmNavigationPropertyInfo();
partsProperty.TargetMultiplicity = EdmMultiplicity.Many;
partsProperty.Target = partType;
partsProperty.ContainsTarget = false;
partsProperty.OnDelete = EdmOnDeleteAction.None;
partsProperty.Name = "Parts";
var carsProperty = new EdmNavigationPropertyInfo();
carsProperty.TargetMultiplicity = EdmMultiplicity.Many;
carsProperty.Target = carType;
carsProperty.ContainsTarget = false;
carsProperty.OnDelete = EdmOnDeleteAction.None;
carsProperty.Name = "Cars";
var nav = EdmNavigationProperty.CreateNavigationPropertyWithPartner(partsProperty, carsProperty);
carType.AddProperty(nav);
config.Routes.MapODataRoute("odata", "odata", edmModel);
while this allowed me to invoke the above speciefied method trough the also above specified URL, it gave me the following error:
{
"odata.error":{
"code":"","message":{
"lang":"en-US","value":"An error has occurred."
},"innererror":{
"message":"The 'ObjectContent`1' type failed to serialize the response body for content type 'application/json; odata=fullmetadata; charset=utf-8'.","type":"System.InvalidOperationException","stacktrace":"","internalexception":{
"message":"The related entity set could not be found from the OData path. The related entity set is required to serialize the payload.","type":"System.Runtime.Serialization.SerializationException","stacktrace":" at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteObject(Object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)\r\n at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, HttpContent content, HttpContentHeaders contentHeaders)\r\n at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext, CancellationToken cancellationToken)\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.GetResult()\r\n at System.Web.Http.WebHost.HttpControllerHandler.<WriteBufferedResponseContentAsync>d__1b.MoveNext()"
}
}
}
}

You have to call "AddNavigationTarget" on the EntitySet.
Assume that your namespace is "MyNamespace", then add the following code to your WebApiConfig.cs. In this way, retrieving the data with "Get: odata/Cars(1)/Parts" will work.
var cars = (EdmEntitySet)edmModel.EntityContainers().Single().FindEntitySet("Cars");
var parts = (EdmEntitySet)edmModel.EntityContainers().Single().FindEntitySet("Parts");
var carType = (EdmEntityType)edmModel.FindDeclaredType("MyNamespace.Car");
var partType = (EdmEntityType)edmModel.FindDeclaredType("MyNamespace.Part");
var partsProperty = new EdmNavigationPropertyInfo();
partsProperty.TargetMultiplicity = EdmMultiplicity.Many;
partsProperty.Target = partType;
partsProperty.ContainsTarget = false;
partsProperty.OnDelete = EdmOnDeleteAction.None;
partsProperty.Name = "Parts";
cars.AddNavigationTarget(carType.AddUnidirectionalNavigation(partsProperty), parts);

Taking #FengZhao's answer further, in order to get the url odata/Cars working you also need to register the navigation property link builder to entity set link builder.
var cars = (EdmEntitySet)edmModel.EntityContainers().Single().FindEntitySet("Cars");
var parts = (EdmEntitySet)edmModel.EntityContainers().Single().FindEntitySet("Parts");
var carType = (EdmEntityType)edmModel.FindDeclaredType("MyNamespace.Car");
var partType = (EdmEntityType)edmModel.FindDeclaredType("MyNamespace.Part");
var partsProperty = new EdmNavigationPropertyInfo();
partsProperty.TargetMultiplicity = EdmMultiplicity.Many;
partsProperty.Target = partType;
partsProperty.ContainsTarget = false;
partsProperty.OnDelete = EdmOnDeleteAction.None;
partsProperty.Name = "Parts";
var navigationProperty = carType.AddUnidirectionalNavigation(partsProperty);
cars.AddNavigationTarget(navigationProperty, parts);
var linkBuilder = edmModel.GetEntitySetLinkBuilder(cars);
linkBuilder.AddNavigationPropertyLinkBuilder(navigationProperty,
new NavigationLinkBuilder((context, property) =>
context.GenerateNavigationPropertyLink(property, false), true));

Web Api cannot resolve your URL against any of registered URI templates.
Use Route Debugger to figure this out.
http://blogs.msdn.com/b/webdev/archive/2013/04/04/debugging-asp-net-web-api-with-route-debugger.aspx
I believe our problem is the id parameter that you pass via url
Try making it more explicit
config.Routes.MapHttpRoute
(
"DefaultInternalApi",
"api/{controller}/{objectType}/{Id}/{relation}",
defaults:
new
{
Id = System.Web.Http.RouteParameter.Optional,
}
);

The original question could also have solved their issue by adding Parts as a property on the Car object if it made sense for them. That is, adding the navigation property for real rather than persuading the OData model builder to register it. For example:
public class Car
{
public int Id {get;set;}
public string Name {get;set;}
public virtual ICollection<PartState> PartStates {get;set; }
public virtual ICollection<Part> Parts { get => this.PartStates?.Select(partState => partState.Part)
}

Related

Execute request model binding manually

In Asp.Net you can automatically parse request data into an input model / API contract using for example the attributes FromBody, FromQuery, and FromRoute. I want to execute this behavior myself. Let me explain.
I want to have a custom policy requirement based on a combination of data passed to the requirement and the target entity which is passed inside the request data. But this target entity id can be in different locations. Usually the body, but for example the route or the query when using HttpGet. So I thought about putting this information about the location above the controller endpoint using an attribute. The following pseudo-code is based on the guess that I need the BindingSource.
I would create API contracts using an interface defining the location of the target id.
public interface ITargetEntityContract {
public string TargetEntityId { get; set; }
}
public class ExampleRequest : ITargetEntityContract {
public string TargetEntityId { get; set; } = default!;
public string SomeOtherData { get; set; } = default!;
}
Then I would create an attribute to define the location:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class TargetEntityLocationAttribute : Attribute {
public Type ContractType { get; }
public BindingSource BindingSource { get; }
public TargetEntityLocationAttribute(Type contractType, BindingSource bindingSource) {
if (!typeof(ITargetEntityContract).IsAssignableFrom(contractType))
throw new Exception("Contract has to implement the interface ITargetEntityContract");
this.ContractType = contractType;
this.BindingSource = bindingSource;
}
}
And you would apply this onto a controller endpoint the following way:
[TargetEntityLocation(typeof(ExampleRequest), BindingSource.Body)]
public async Task<IActionResult> SomeEndpointAsync([FromBody] ExampleRequest requestData) {
}
Within the IAuthorizationHandler I would use these classes the following way:
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ExampleAuthorizationRequirement requirement) {
var endpoint = this._httpContextAccessor.HttpContext!.GetEndpoint();
if (endpoint is null)
throw new Exception("Some error");
var targetEntityLocation = endpoint.MetaData.GetMetaData<TargetEntityLocationAttribute>();
if (targetEntityLocation is null)
throw new Exception("some error");
var targetEntityModel = SomeAlmightyParser.ParseFromSource(this._httpContextAccessor.HttpContext!, targetEntityLocation.ContractType, targetEntityLocation.BindingSource) as ITargetEntityContract;
// do something with targetEntityModel.TargetEntityId
}
Is there a way to parse the request data of the HttpContext into the given model based on the data location?
If the source of the data is RequestBody,you could read the stream with StreamReader and deserialize the string you get;
If the source of the data is RequestQuery,you could map the key-value pairs to your target object with reflection
For example,I tried as below:
//read obj from requestbody
using var reader = new StreamReader(context.Request.Body);
var bodystr = reader.ReadToEndAsync().Result;
if (bodystr != "")
{
var obj1 = JsonSerializer.Deserialize(bodystr, typeof(ExampleRequest), new JsonSerializerOptions() { });
}
//read simple obj from query
var querycollection = context.Request.Query;
var type = typeof(ExampleRequest);
var properties = type.GetProperties();
object obj = Activator.CreateInstance(type);
foreach (var propinfo in properties)
{
var propname = propinfo.Name;
if (querycollection.ContainsKey(propname))
{
var proptype = propinfo.PropertyType;
propinfo.SetValue(obj, Convert.ChangeType(querycollection[propname].ToString(), proptype),null);
}
}
var obj2 = obj as ExampleRequest;
Result:

Ignore XML namespace when modelbinding in asp net core web api

I know this has been asked here and at various other places but I have not seen a simple answer.
Or at least, I have not been able to find any.
In short, I have an .Net Core Web Api endpoint that accepts XML.
Using (in Startup):
services.AddControllers().AddXmlSerializerFormatters();
I want to modelbind it to a class. Example:
[Route("api/[controller]")]
[ApiController]
public class PersonController : ControllerBase
{
[HttpPost]
[Consumes("application/xml")]
[ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Post))]
public async Task<ActionResult> PostPerson([FromBody] Person person)
{
return Ok();
}
}
// Class/Model
[XmlRoot(ElementName = "Person")]
public class Person
{
[XmlElement(ElementName = "Name")]
public string Name { get; set; }
[XmlElement(ElementName = "Id")]
public int Id { get; set; }
}
Passing in:
<Person><Name>John</Name><Id>123</Id></Person>
works fine. However, as soon as namespaces comes into play it either fails to bind the model:
<Person xmlns="http://example.org"><Name>John</Name><Id>123</Id></Person>
<Person xmlns="http://example.org"><Name>John</Name><Id xmlns="http://example.org">123</Id></Person>
Or the model can be bound but the properties are not:
<Person><Name xmlns="http://example.org">John</Name><Id>123</Id></Person>
<Person><Name xmlns="http://example.org">John</Name><Id xmlns="http://example.org">123</Id></Person>
etc.
I understand namespaces. I do realize that I can set the namespaces in the XML attribute for the root and elements.
However, I (we) have a dozens of callers and they all set their namespaces how they want. And I want to avoid to have
dozens of different versions of the (in the example) Person classes (one for each caller). I would also mean that if a caller
changes their namespace(s) I would have to update that callers particular version and redeploy the code.
So, how can I modelbind incoming XML to an instance of Person without taking the namespaces into account?
I've done some tests overriding/creating an input formatter use XmlTextReader and set namespaces=false:
XmlTextReader rdr = new XmlTextReader(s);
rdr.Namespaces = false;
But Microsoft recommdes to not use XmlTextReader since .Net framework 2.0 so would rather stick to .Net Core (5 in this case).
You can use custom InputFormatter,here is a demo:
XmlSerializerInputFormatterNamespace:
public class XmlSerializerInputFormatterNamespace : InputFormatter, IInputFormatter, IApiRequestFormatMetadataProvider
{
public XmlSerializerInputFormatterNamespace()
{
SupportedMediaTypes.Add("application/xml");
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
var xmlDoc = await XDocument.LoadAsync(context.HttpContext.Request.Body, LoadOptions.None, CancellationToken.None);
Dictionary<string, string> d = new Dictionary<string, string>();
foreach (var elem in xmlDoc.Descendants())
{
d[elem.Name.LocalName] = elem.Value;
}
return InputFormatterResult.Success(new Person { Id = Int32.Parse(d["Id"]), Name = d["Name"] });
}
}
Person:
public class Person
{
public string Name { get; set; }
public int Id { get; set; }
}
startup:
services.AddMvc(options =>
{
options.RespectBrowserAcceptHeader = true; // false by default
options.InputFormatters.Insert(0, new XmlSerializerInputFormatterNamespace());
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddXmlSerializerFormatters()
.AddXmlDataContractSerializerFormatters();
result:
So, in order to be able to modelbind XML to a class without taking namespaces into consideration I created new InputFormatter.
And I use XmlTextReader in order to ignore namespaces. Microsoft recommends to use XmlReader rather than XmlTextReader.
But since XmlTextReader is there still (in .Net 6.0 Preview 3) I'll use it for now.
Simply create an inputformatter that inherits from XmlSerializerInputFormatter like so:
public class XmlNoNameSpaceInputFormatter : XmlSerializerInputFormatter
{
private const string ContentType = "application/xml";
public XmlNoNameSpaceInputFormatter(MvcOptions options) : base(options)
{
SupportedMediaTypes.Add(ContentType);
}
public override bool CanRead(InputFormatterContext context)
{
var contentType = context.HttpContext.Request.ContentType;
return contentType.StartsWith(ContentType);
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
var type = GetSerializableType(context.ModelType);
var request = context.HttpContext.Request;
using (var reader = new StreamReader(request.Body))
{
var content = await reader.ReadToEndAsync();
Stream s = new MemoryStream(Encoding.UTF8.GetBytes(content));
XmlTextReader rdr = new XmlTextReader(s);
rdr.Namespaces = false;
var serializer = new XmlSerializer(type);
var result = serializer.Deserialize(rdr);
return await InputFormatterResult.SuccessAsync(result);
}
}
}
Then add it to the inputformatters like so:
services.AddControllers(o =>
{
o.InputFormatters.Add(new XmlNoNameSpaceInputFormatter(o));
})
.AddXmlSerializerFormatters();
Now we can modelbind Person or any other class no matter if there is namespaces or not in the incoming XML.
Thanks to #yiyi-you

Passing values from Dynamics CRM entity form to external web service through plug-in execution method

I want to pass to external web service some values of an entity (Case/incident) while new record is going to be created.
I have a model for preparing data which have to be sent to web service as below:
public class TicketViewModel
{
public string CaseID { get; set; }
public string Subject { get; set; }
public string Description { get; set; }
public string CreateTime { get; set; }
public string Owner { get; set; }
public string States { get; set; }
public string Assigned { get; set; }
}
Here is my code inside Execute() method:
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = factory.CreateOrganizationService(context.UserId);
if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity)
{
try
{
var entity = (Entity)context.InputParameters["Target"];
if (entity.LogicalName != "incident") // The logical name for Case entity
return;
Guid recordID = entity.Id;
var ticket = new CaseViewModel
{
// Retrieving Intended Fields Value
};
BasicHttpBinding httpBinding = new BasicHttpBinding();
httpBinding.Name = "HttpBinding_Service";
httpBinding.Security.Mode = BasicHttpSecurityMode.None;
httpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None;
httpBinding.Security.Transport.ProxyCredentialType = HttpProxyCredentialType.None;
httpBinding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.UserName;
EndpointAddress epa = new EndpointAddress(#"webservice/url/address");
CallChamberPortalSoapClient tcClient = new CallChamberPortalSoapClient(httpBinding, epa);
var res = tcClient.addTicket(//Passing Intended Fields Value);
entity["X"] = res.ToString();
}
catch (Exception ex)
{
throw new InvalidPluginExecutionException("Failed to register ticket by this error: " + ex.Message);
}
My first question is how to retrieve intended fields value on Create new record? I have used entity["X"] to get value of "X" field but nothing returned.
My second question is how to set value of a field on Update a record? Using same expression ( entity["X"] = "NewValue" ) not worked for me.
Note: sample static data has sent to web service successfully and it returned true as result.
EDIT:
I tried to get values as below but have error in CRM create record event.
ColumnSet cs = new ColumnSet(new string[] {
"ticketnumber", "title", "description", "createdon", "customerid", "new_peygiriii", "createdby" });
Entity wholeCase = service.Retrieve("incident", recordID, cs);
Owner = wholeCase.GetAttributeValue<EntityReference>("customerid").ToString();
Error:
Unable to cast object of type Microsoft.Xrm.Sdk.OptionSetValue to type
Microsoft.Xrm.Sdk.EntityReference
Thanks.
First, You should register your plugin in Dynamics as Post operation (create). Reason once the record is created in System, you will get it's Guid and so on. This is best way and in addition make your plugin Asynchronous (syn only if it is a real must for your use case).
Now when you create a recrod in crm plugin will get it's context as you are doing.
var entity = (Entity)context.InputParameters["Target"];
now you can get particualr fileds value, you do something like below
if(entity.contains("field name")){
var recordName=entity.GetAttributeValue<string>("field name");
}
if you want optionset values you do something like below
if(entity.contains("optionset field name")){
int selectedTopic = entity.GetAttributeValue<OptionSetValue>("optionset field name").Value
String text = entity.FormattedValues["optionset field name"].ToString();
}
To set up? what type of data you want to set up, assuming you want to set up optionset value
entity["X"] = new OptionSetValue(INDEX)
The INDEX is an int you can look up in your optionset editor (default values are several digit long).

Upload a file and also pass model data

I need to import a file, and also the Person model that I have shown below.
I am able to upload the file, However, I am not able to retrieve the Person model data in the importFileAndOtherInfo method that I have written.
Note: I am testing this web API via Postman. How can I upload a file, and also send Person Model data via Postman?
Person
int pId
string PName
School schoolAttended
My implementation:
[HttpPost]
public async Task<int> importFileAndOtherInfo(Person person)
{
var stream = HttpContext.Current.Request.Files[0].InputStream
// HOW TO RETRIEVE THE PERSON DATA HERE.
}
What I understood from your question is, you want to pass model data and the file in stream at the same time; you can't send it directly, the way around is to send file with IFormFile and add create your own model binder as follows,
public class JsonWithFilesFormDataModelBinder: IModelBinder
{
private readonly IOptions<MvcJsonOptions> _jsonOptions;
private readonly FormFileModelBinder _formFileModelBinder;
public JsonWithFilesFormDataModelBinder(IOptions<MvcJsonOptions> jsonOptions, ILoggerFactory loggerFactory)
{
_jsonOptions = jsonOptions;
_formFileModelBinder = new FormFileModelBinder(loggerFactory);
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
// Retrieve the form part containing the JSON
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.FieldName);
if (valueResult == ValueProviderResult.None)
{
// The JSON was not found
var message = bindingContext.ModelMetadata.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(bindingContext.FieldName);
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, message);
return;
}
var rawValue = valueResult.FirstValue;
// Deserialize the JSON
var model = JsonConvert.DeserializeObject(rawValue, bindingContext.ModelType, _jsonOptions.Value.SerializerSettings);
// Now, bind each of the IFormFile properties from the other form parts
foreach (var property in bindingContext.ModelMetadata.Properties)
{
if (property.ModelType != typeof(IFormFile))
continue;
var fieldName = property.BinderModelName ?? property.PropertyName;
var modelName = fieldName;
var propertyModel = property.PropertyGetter(bindingContext.Model);
ModelBindingResult propertyResult;
using (bindingContext.EnterNestedScope(property, fieldName, modelName, propertyModel))
{
await _formFileModelBinder.BindModelAsync(bindingContext);
propertyResult = bindingContext.Result;
}
if (propertyResult.IsModelSet)
{
// The IFormFile was successfully bound, assign it to the corresponding property of the model
property.PropertySetter(model, propertyResult.Model);
}
else if (property.IsBindingRequired)
{
var message = property.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(fieldName);
bindingContext.ModelState.TryAddModelError(modelName, message);
}
}
// Set the successfully constructed model as the result of the model binding
bindingContext.Result = ModelBindingResult.Success(model);
}
}
Model
[ModelBinder(typeof(JsonWithFilesFormDataModelBinder), Name = "data")]
public class Person
{
public int pId {get; set;}
public string PName {get; set;}
public School schoolAttended {get; set;}
public IFormFile File { get; set; }
}
Postman request:
I am using the same in netcoreapp2.2. Works successfully.
Now when migrating from notecoreapp2.2 to netcoreapp3.1 I'm facing issues with private readonly IOptions<MvcJsonOptions> _jsonOptions;
As MvcJsonOptions is a breaking change from core 2.2 to 3.0.
Check this:
Migration of netcoreapp2.2 to netcoreapp3.1 - convert MvcJsonOptions to core3.1 compatable

How to map two list with automapper?

I have two classes in the different location.
namespace IVR.MyEndpointApi.POCO
{
[Table("MyServiceUrl")]
public class MyURL
{
[Key]
[Column("FacilityID")]
public int FacilityId { get; set; }
[Column("Url")]
public string Url { get; set; }
}
}
namespace OpsTools.Models
{
public class MyServiceEndpoint
{
public int FacilityId { get; set; }
public string Url { get; set; }
}
}
In another method, I get the list and want to convert then return it as the desired type. I manually do it as below:
public List<MyServiceEndpoint> GetAllUrls()
{
var management = GetMyEndpointManagement();
var list = management.GetAllUrls();
var urlList = new List<MyServiceEndpoint>();
foreach (var item in list)
{
// the type of item is MyURL
var MyUrl = new MyServiceEndpoint();
myUrl.FacilityId = item.FacilityId;
myUrl.Url = item.Url;
urlList.Add(myUrl);
}
return urlList;
}
My question: can I apply AutoMapper to it?
EDIT:
I used the code:
var myUrls = management.GetAllUrls();
var urlList = new List<MyServiceEndpoint>();
Mapper.CreateMap<MyServiceEndpoint, MyURL>();
urlList = Mapper.Map<List<MyServiceEndpoint>, List<MyURL>>(myUrls);
Mapper.AssertConfigurationIsValid();
However, it has the error:
Error CS1503 Argument 1: cannot convert from 'System.Collections.Generic.List' to ....
Oops, come on. I change the order, then it works.
From
urlList = Mapper.Map<List<MyServiceEndpoint>, List<MyURL>>(myUrls);
To
urlList = Mapper.Map<List< List<MyURL>,MyServiceEndpoint>>(myUrls);
If you inspect the actual exception (you left the relevant part off), you'll see that Mapper.Map<TSource, TDestination>() tries to map from and to the wrong types.
The full error will read:
cannot convert from System.Collections.Generic.List<MyURL> to System.Collections.Generic.List<MyServiceEndpoint>
Which means that call will actually try to map from List<MyServiceEndpoint>, requiring an argument of that type, which your source list isn't.
Simply switch the types in the Map() call:
urlList = Mapper.Map<List<MyURL>, List<MyServiceEndpoint>>(myUrls);
Or remove the new list creation entirely, move the declaration and use type inference:
var urlList = Mapper.Map<List<MyServiceEndpoint>>(myUrls);

Categories

Resources