How can I create an UpdateMany endpoint? - c#

The Issue
I am trying to create an endpoint for my API that will take a list of DTOs and update each of them.
What I've Tried
In my first attempt, I made a method with the HttpPatch attribute that took a list of the given DTO as its parameter, and the DTOs were correctly filled with the data I sent (and had null/default values for any of the fields I didn't send). However, this leaves me with the issue of not knowing what values have been actually changed since this way I cannot tell the difference between a field I want changed to null and a field that is not being updated.
Thus, in my second attempt I used a list of Delta of the given DTO, using Delta class. This allowed me to discern which fields were being changed in the DTOs provided- however, it inexplicably no longer includes the primary ID key, so it is impossible to update my data with the changed information I have now isolated.
References
Below is a paraphrased example of my code and a link to the Delta class documentation. Please let me know if there is anything I can clarify, and thanks in advance for your help!
Code Example
[HttpPatch]
[Route(".../UpdateMany")]
public HttpResponseMessage UpdateMany(List<Delta<Dto>> deltaDtos) // My first implementation had 'List<Dto> dtos' as the parameter
{
var dtos = new List<Dto>();
var changesList = new List<KeyValuePair<Guid, List<string>>>();
foreach (Delta<Dto> dto in deltaDtos) // This for loop extracts the DTOs from the Deltas and makes a list of lists of changes keyed to their respective DTO ID.
{
var updatedProperties = dto.GetChangedPropertyNames().ToList(); // The updated properties contains a list of the changed field(s), but does not include the ID.
changesList.Add(new KeyValuePair<Guid, List<string>>(dto.GetEntity().ID, updatedProperties));
dtos.Add(dto.GetEntity()); // dto.GetEntity() returns an instance of Dto with the changed fields filled out- not including the ID field for some reason, but including the changed field(s).
}
_service.UpdateMany(dtos, changesList); // The service handles the updating logic.
return Request.CreateResponse(HttpStatusCode.OK);
}
System.Web.Http.OData.Delta Documentation
https://learn.microsoft.com/en-us/previous-versions/aspnet/jj890572(v=vs.118)

Related

AutoMapping from one record to another record with one different type parameter

Context
Our application uses the API Aggregator pattern, means we have an API Aggregator that gets requests from the frontend and then calls multiple microservices (AuthorAPI, BookAPI) to get (and post) the required data. (Book and author are just examples here)
In my code, I the following record type
public sealed record CreateBookDto(
string Title,
string AuthorId
);
This record is used by the frontend to create a book and is the data sent to the API Aggregator.
Then I have the following record type
public sealed record CreateBookWithAuthorInformationDto(
string Title,
Author Author
);
This record is the one that is being sent to the BookAPI already containing the author object (it get's updated through a MessageQueue so we always have the up2date information about the author). So the frontend sends the first DTO to the API. The Aggregator would then call the AuthorAPI to get the Author object and post the data to the BookAPI with the completed author data. This is in order that the BookAPI doesn't need to directly communicate with the AuthorAPI and is all done via the Aggregator pattern.
I now want to use AutoMapper to map the one object to the other
Implementation
So to do this, I created a simple Map like this CreateMap<CreateBookDto, CreateBookWithAuthorInformationDto>()
However, when I then try to map the one to the other (and load the data) like this
var author = await _authorService.GetAuthorByIdAsync(
createBookDto.AuthorId
);
var tempDto = _mapper.Map<CreateBookDto, CreateBookWithAuthorInformationDto>(createBookDto);
var bookWithAuthorDto = tempDto with
{
Author = author,
};
However, this call already fails at var tempDto = _mapper.Map<CreateBookDto, CreateBookWithAuthorInformationDto>(createBookDto); with the error
System.ArgumentException: CreateBookWithAuthorInformationDto needs to have a constructor with 0 args or only optional args. Validate your configuration for details. (Parameter 'type')
So what I found is to use ConstructUsing in the Profile, which I did like this
CreateMap<CreateBookDto, CreateBookWithAuthorInformationDto>().ConstructUsing(
x => new CreateBookWithAuthorInformationDto(
x.Title,
// PROBLEM, READ BELOW
)
);
However, of course I cannot assign the Author in there, because I don't have the author yet. Sure, I could write new Author(), however, Author may have constructor arguments as well, with more constructor arguments and so on and so on.
I also can't write null in there, because Author is not nullable and I don't want to make it nullable, because it shouldn't be nullable. This information MUST be there and I don't want to add a nullable possibility just to have it work with Automapper.
Additionally I tried using a IMappingAction, however the IMappingAction would be AFTER the map and also it doesn't support async processes, which my API requires.
Thoughts
So... what I found is that I can easily map 2 records to each other, when using ConstructUsing. However I see this as not the best way to do it because whenever I add properties to the DTO I have to adjust the Profile. AutoMaper should do the work automatically, otherwise I could just write it myself. However, I also need to make calls to apply them to the DTO which also doesn't work, because it is not nullable and shouldn't be.
I also don't want the Frontend to send the whole Author object because the frontend could change the data, which means I would need to query the data anyway to verify it is correct. The frontend also needs only the name of the author and the id (for displaying) so I also, to reduce the payload, want to send only the data that is really required. The Aggregator should take care of the rest.
So how would I solve this issue?

acumatica c# / Add SOPackageDetailEx

I have this error when I try to add a line of package
Error : Another process has added the "SOPackagedetail" record. Your changes will be lost.
error
My c# code is this :
protected virtual void creationColis()
{
SOShipment ship=Base.CurrentDocument.Select();
SOPackageDetailEx colis = new SOPackageDetailEx();
colis.BoxID="COLIS";
colis.PackageType="M";
colis.ShipmentNbr=ship.ShipmentNbr;
SOShipmentEntry graph = PXGraph.CreateInstance<SOShipmentEntry>();
graph.Packages.Insert(colis); //insertion de l'enregistrement
graph.Packages.Update(colis);
graph.Actions.PressSave();
graph.Clear();
}
Do you know what I must to change please ?
Thanks so much
Xavier
Your question needs more context. For starters, where does your code reside? Given that you reference Base.CurrentDocument.Select, I'm going to assume you are extending SOShipmentEntry to add your code.
In this case, you would just use the Base.Packages view rather than initializing your own instance of SOShipmentEntry where your example goes into trying to use graph.Packages. Regardless, there are 2 parts here that need to be addressed.
Packages is not the primary view of SOShipmentEntry. When you create an instance of a graph, you must tell the graph what record is needed in the primary view. In your example where you create a new instance of a graph, you might do something like this:
graph.Document.Current = graph.Document.Search<SOShipment.shipmentNbr>(myShipmentNbr);
If you are working on a graph extension of SOShipmentEntry, then you probably don't need to create a new instance of the graph. Just make sure graph.Document.Current isn't null before you add your package record - see bullet 2.
Once you have a shipment selected, you can then insert your package information. However, the way you have done it here effectively is trying to add a random package to a null shipment (by the structure of the views) but forcing the record to attach to the right shipment by sheer brute force. The views don't like to work that way.
A better way to add your package once you have a current shipment (Document) is like this:
// Find the current shipment (from the primary view Document)
SOShipment ship = Base.Document.Current();
if(ship?.ShipmentNbr != null) {
// Insert a record into the Packages view of the current shipment and return the record into colis
SOPackageDetailEx colis = Base.Packages.Insert(colis);
// Set the custom values
colis.BoxID="COLIS";
colis.PackageType="M";
// Update the Packages cache with the modified fields
Base.Packages.Update(colis);
// If more fields need to be updated after those changes were applied, instead do this...
colis = Base.Packages.Update(colis);
colis.FieldA = ValueA;
colis.FieldB = ValueB;
Base.Packages.Update(colis);
// If a save is needed, now is the time
Base.Save.Press();
}
Notice that I didn't assign ShipmentNbr. That is because the DAC has that field defined to pull the ShipmentNbr from SOShipment through these 2 attributes.
[PXParent(typeof(FK.Shipment))]
[PXDBDefault(typeof(SOShipment.shipmentNbr))]
This means that when the record is created, Acumatica should lookup the parent SOShipment record via the Key and do a DBDefault on the field to assign it to the SOShipment.ShipmentNbr value (from the parent). Important side note: PXDefault and PXDBDefault are NOT interchangeable. We use PXDefault a lot, but off the top of my head I can't think of a case of PXDBDefault outside of defaulting from a database value like this specific usage.

How to update variable columns based on json object from body? (C# web api, LINQ to SQL)

So right now I can update a record using this code:
//POST: api/Cicmpies/testname
[HttpPost("{cmp_name}")]
public async Task<IActionResult> UpdateCicmpy([FromRoute] string cmp_name, [FromBody] Cicmpy cicmpy)
{
var cicmpyUpdated = await
(from c in _context.Cicmpy
where c.CmpName.Equals(cmp_name)
select c).SingleOrDefaultAsync();
cicmpyUpdated.CmpCode = cicmpy.CmpCode;
await _context.SaveChangesAsync();
return Ok(cicmpy);
}
To achieve this I send a json object using Postman like this:
{
"cmpCode": "testcodeupdated333"
}
To the following URL:
http://localhost:54488/api/Cicmpies/testname
This works since I know I'll only have to update "CmpCode" so I can do:
cicmpyUpdated.CmpCode = cicmpy.CmpCode;
What if I don't know what values will have to be updated? So sometimes the json object can contain 1 key-value pair (CmpCode), but sometimes it can contain 4, sometimes all 20, etc... How can I ensure "cicmpyUpdated" will always set all the values from howmany key-value pairs you entered in your json object?
you can do 3 things:
1.- use default values. Give some default values and then update everything
2.- set a maximum of n pairs per object and then send several objects.
3.- keep checking: example: while (more pairs){do whatever}
Generally, the proper way to handle this is to establish a “ViewModel” class for your input parameter on your action method. One option is to give your viewmodel the same properties of your entity and make those viewmodel properties nullable so that if the input doesn’t provide a value you can add a check in your controller to avoid setting the model property if the input viewmodel property is null. Alternatively you could add a custom model binder to set additional properties on your viewmodel that tell you which fields were present in the Json data.

Only update some properties on an EF entity that are not set to null

I've got a browser sending up JSON but it only includes the properties of a given model that have been changed. So once the WCF DataContractJsonSerializer does it's work I have an object that will have perhaps only the ID and Description fields populated.
Attaching this to the DbContext as is will result in the description field being updated but all the other fields being set to their types default value in the database. This is because if WCF doesn't see the property specified in the JSON then it'll skip over it, meaning the property in the instance will just use the types default value as per the auto-implemented property.
So this means that I need to decide on which fields have been passed up without having access to the JSON itself. In fact it could be XML on the wire so all I can work from is this partly serialized object.
What seems most logical is to use null as the special value that means this property hasn't been serializd over. So in the constructor of the POCO model I set all the properties to null.
In the Update method I then use this serialized object as a stub. I have to walk each property and if the value isn't set to null then I set it's state to modified. As far as I can tell this is working without any side effects but I'm just not sure that this is the way to do something like this.
One limitation it does add is that the client can no longer intentionally set a property to null as that update would be lost. One way around this is to have a special int value that can be set to represent null in the database and perhaps an empty string to represent null in the database and have code in the update to look for these special values and then set the entity property to null. Far from ideal and likely to be prone to bugs.
Here is the code I currently have to process the update. I would really really appreciate advice as to a better, perhaps more obvious way of doing this.
To Summerise: How can I tell which properties on an model instance have been set by the DataContractSerializer/DataContractJsonSerializer and which are just using default values from it's constructor. Using special values is problematic as the client might want to set something to an empty string, or to 0 or -1 or indeed to null.
public T Update(T obj)
{
var entity = ds.Attach(obj);
// For each property in the model
foreach (var p in typeof(T).GetProperties())
{
// Get the value of the property
var v = p.GetValue(obj, null);
// Assume null means that the property wasn't passed from the client
if (v == null)
continue;
// Set this property on the entity to modified unless it's ID which won't change
if (p.Name != "ID")
dc.Entry(entity).Property(p.Name).IsModified = true;
}
dc.SaveChanges();
return entity;
}
UPDATE: Using Hammerstein's answer below to have self tracked models, I've updated my update function as below. Unfortunately due to my use of the Required attribute on the models for pre-save validation EF throws a wobbly when using a stub instance that contains nulls for the non modified values. You would think that EF would realise as part of it's validation that some fields are set to not modified but alas it doesn't so I'm forced to do a read and then update that. Actually this might be a good candidate for a separate question to post to try and avoid the read.
public virtual T Update(T obj)
{
var entity = ds.Find(obj.ID);
((TrackedModel)obj).Modified.ForEach(p => {
var prop = dc.Entry(entity).Property(p.PropertyName);
prop.CurrentValue = p.NewValue;
prop.IsModified = true;
});
dc.SaveChanges();
return entity;
}
My solution to this problem was a tracked change model, I created an abstract base class that had a list of strings, then for each property on my model, I called a method NotifyChanged( "MyProperty") which added the property to the list.
Because model binding will only set the fields that have been posted back, you should get an accurate list of fields that changed.
Then I loop through the list, and set the values on my entity.
Not clean, but it worked for my purpose.
Update: My solution did require me to get away from auto-properties and to hand write them. In the setter, after setting the value, I call NotifyChanged. I'm using this with MVC regular model binding, I don't think I have a working example of passing an object as JSON and deserializing. You could look at JSON.NET, controlling the serialization/deserialization I believe you can tell it to ignore default property values etc.

C# - Get property in member class using Reflection

SHORT VERSION
What's the best way to use reflection to turn something like string prop = "part1.first_name"; into a System.Reflection.PropertyInfo, so that I can use the GetValue and SetValue functions?
LONG VERSION
I'm using ASP .NET MVC to build a questionnaire for my organization. It's very long, so it's divided into several different pages. Since it's not uncommon for us to get requests like, "Can you move this question to that page, and this other question to another page," I need to build this to be pretty flexible for a junior programmer to change.
My model is a complex class (it's got five member classes that have mostly primitive-typed properties on them).
So, I access it by doing things like Model.part1.first_name or Model.part2.birth_date.
Since the same model is used on all of the pages, but not all of the questions are on every page, I have ActionAttributes that essentially clear out all of the properties that were submitted on the form except for the ones that were displayed on that page (so someone can't inject a hidden field into the form and have the value persist to the database).
I want to make sure that I only save valid field values and don't let the user proceed to the next page until the current one is entirely OK, but I also want to save the values that are valid, even if the user isn't allowed to proceed.
To do this, I have a function that takes two instances of my model class, a reference to the ModelStateDictionary, and a string[] of field names like "part1.first_name" and "part2.birth_date". That function needs to copy all of the values listed in the string array that do not have validation errors from the first (ie, form-submitted) object into the second (ie, loaded from the db) object.
As stated above, what's the best way to use reflection to turn something like "part1.first_name" into a System.Reflection.PropertyInfo, OR, is there a better way to accomplish this?
var infoParts = prop.Split('.');
var myType = Type.GetType(infoParts[0]);
var myPropertyInfo = myType.GetProperty(infoParts[1]);
Assuming "part1" is your type. Although this is very limited and very dependent on the string being in the correct format and the type being in the current scope.
I would probably handle this differently, using data. I would keep, in the database, which step each question belongs to. To render that step, I would select the questions that match that step and have a model that contains a list of question id/question pairs. Each input would be identified by the question id when posted back. To validate, simply compare the set of question ids with the expected ids for that step. This way, to change which question goes in which step is to only change the data in the database.
If you do end up going down that road, you'll need to split the string into parts and recursively or iteratively find the property on the object at each step.
PropertyInfo property = null;
Type type = questionModel.GetType();
object value = questionModel;
object previousObj = null;
foreach (var part in questionId.Split('.'))
{
property = type.GetProperty(part);
previousObj = value;
value = property.GetValue(value,null);
type = value.GetType();
}
// here, if all goes well, property should contain the correct PropertyInfo and
// value should contain that property's value...and previousObj should contain
// the object that the property references, without which it won't do you much good.

Categories