Web API: Should the body of my POST method contains the identifier? - c#

What is the common rule or best practices about creating (POST method) a new item in my database with a Web API (REST). Should the body of my POST method contains the identifier?
With identifier:
public HttpResponseMessage Post(AddressModel addressModel)
{
using (var context = new DbContext())
{
if (context.address.Any(y => y.id == addressModel.id)
throw new Exception("Item already exist"); // or return BadRequest
...
}
}
Without identifier:
public HttpResponseMessage Post(AddressModel addressModel)
{
if (addressModel.id != null)
throw new Exception("identifier cannot be set"); // or return BadRequest
using (var context = new DbContext())
{
addressModel.id = GetNewId();
// GetNewId generate the next correct if from data
// from context.address.last ...
...
}
}
And of course (before downvoting) you can just tell me there is no correct answer because the first solution is more efficient in this or this case and the other one is better for other reason. Of course I have my own opinion and this is an opinion question for people without experience like me. But for people with experience I'm sure they can answer this question without hesitation and with non opinionated arguments. So this is not en opinionated question or please tell me why.

If you are creating an object, the API should be responsible for assigning the unique id. Otherwise, the caller has to know too much about how the API works (what type of id, how do we get unique values, etc.).
Also, you can return the assigned if if the caller needs it for later operations.

Related

How to disallow a post method from Get method in Web API?

I am just learning about WebAPIs and curious if we can reuse the Post method inside get method or it just violates the coding standards. How can we test if this violation is already done by someone?
// GET api/values/5
public string Get(int id)
{
var value= vc.Values.Where(v => v.Id == id).Select(v => v.Value1).SingleOrDefault();
if (value==null) Post("New Value",id);
return vc.Values.Where(v => v.Id == id).Select(v => v.Value1).SingleOrDefault();
}
// POST api/values
public void Post([FromBody]string value, int id = 0)
{
vc.Values.Add(new Value { Id=id,Value1 = value });
vc.SaveChanges();
}
These are 2 questions, not one.
Reusing code like this is a recipe for disaster.
You can keep your endpoints very slim by moving the code into a library for example. Then you can simply call these new methods from the endpoints and this takes care of the code reuse part.
In terms of how you detect such issues, well, I wouldn't expect a tool to do it for you. You need a mature SDLC, you need code reviews and analysis on what you have already.

Marten Not Storing Entity

I'm working with Marten as my data layer and it's been great so far, but I've run into an issue that just doesn't make sense. I have a simple method that saves a transaction (a purchase) then updates a listing, adding the ID of the transaction to a collection. My problem is, it appears that Marten is not storing my updated listing, although it is storing the transaction.
When I look in the database, the TransactionIds property is null, but if I step through the code, everything seems to execute correctly. Am I doing something wrong here?
public async Task CreateListingTransactionAsync(ListingTransaction transaction)
{
if (transaction == null)
throw new ValidationException("Transaction is required to create a transaction");
bool isNew = transaction.Id == Guid.Empty;
await _listingTransactionValidator.ValidateAndThrowAsync(transaction);
using (var session = _store.LightweightSession())
{
session.Store(transaction);
if (isNew)
{
var listing = await session.LoadAsync<Listing>(transaction.ListingId);
if (listing == null)
throw new EntityNotFoundException($"Listing with Id: {transaction.ListingId} not found");
if (listing.TransactionIds == null)
listing.TransactionIds = new List<Guid>();
listing.TransactionIds.Add(transaction.Id);
session.Store(listing);
}
await session.SaveChangesAsync();
}
}
There could be a problem with the serialization of TransactionIds collection.
If that's not the case then here are some random things to try (and try to understand why it worked later):
Try session.Update(listing); instead of session.Store(listing);.
Try different type of document session. http://jasperfx.github.io/marten/documentation/troubleshoot/

Why did I have to use Task<T> to acheive Synchronous calls?

Im working on a website that integrates with Dynamics 365 with the Dynamics SDK. We have seen errors in the logs such as “Cannot access a disposed object”. Upon further investigation we found out that the SDK methods are not thread safe so needed to refactor the code to take this into account.
We had a method such as follows that would create or update a Contact entity depending on whether it already exists:
public Guid? SetProfile(IProfile profile)
{
using (var xrm = new XrmServiceContext(_organizationService))
{
//check whether account already exists
var crmProfile = GetContact(xrm, profile.UserId);
if (crmProfile == null)
{
//create new account if required
{
crmProfile = new Contact
{
EMailAddress1 = profile.Username,
//lots of properties hidden to make for easier code example
};
}
xrm.AddObject(crmProfile);
}
else
{
//update existing account
crmProfile.new_Title = profile.Title.HasValue ? new OptionSetValue(profile.Title.Value) : null;
//lots of properties hidden to make for easier code example
xrm.UpdateObject(crmProfile);
}
var response = xrm.SaveChanges();
return crmProfile.Id;
}
}
When this method was executed concurrently by 2 or more users the error "Cannot access a disposed object" would be thrown, referring to the XrmServiceContext object.
I therefore knew that I needed to make this method thread-safe, but also it needs to be Synchronous as our UI depends on having the return value of the method. I played around with different threading methods:
Task.Factory.StartNew(() => delegate
new Thread()
However, with both of these methods I wasn't able to get the method to execute synchronously, so I ended up with:
public Guid? SetProfile(IProfile profile)
{
var task = new Task<Guid?>(() =>
{
using (var xrm = new XrmServiceContext(_organizationService))
{
//check whether account already exists
var crmProfile = GetContact(xrm, profile.UserId);
if (crmProfile == null)
{
//create new account if required
{
crmProfile = new Contact
{
EMailAddress1 = profile.Username,
//lots of properties hidden to make for easier code example
};
}
xrm.AddObject(crmProfile);
}
else
{
//update existing account
crmProfile.new_Title = profile.Title.HasValue ? new OptionSetValue(profile.Title.Value) : null;
//lots of properties hidden to make for easier code example
xrm.UpdateObject(crmProfile);
}
var response = xrm.SaveChanges();
return crmProfile.Id;
}
});
task.RunSynchronously();
return task.Result;
}
Everything I seemed to read online suggested I should use the StartNew method, however this is geared towards Asynchronous calls with I could not allow, and it also seemed that it doesnt guarantee a new thread - from what I've read I understand it is clever enough to know when it needs to create a new thread - however in my instance I have to be certain a new thread is used for the call to Dynamics.
Questions:
Anything wrong with the approach I've taken for a Web application?
If I can't use Asynchronous calls, is there any advantage whatsoever to using the StartNew method?
Many thanks for your time in advance
Kind regards
dotdev

Break out of parent function?

public static void CacheUncachedMessageIDs(List<int> messageIDs)
{
var uncachedRecordIDs = LocalCacheController.GetUncachedRecordIDs<PrivateMessage>(messageIDs);
if (!uncachedRecordIDs.Any()) return;
using (var db = new DBContext())
{
.....
}
}
The above method is repeated regularly throughout the project (except with different generics passed in). I'm looking to avoid repeated usages of the if (!uncachedRecordIDs.Any()) return; lines.
In short, is it possible to make the LocalCacheController.GetUncachedRecordIDs return the CacheUncachedMessageIDs method?
This will guarantee a new data context is not created unless it needs to be (stops accidentally forgetting to add the return line in the parent method).
It is not possible for a nested method to return from parent method.
You can do some unhandled Exception inside GetUncachedRecordIDs, that will do the trick, but it is not supposed to do this, so it creates confusion. Moreover, it is very slow.
Another not suggested mechanic is to use some goto magic. This also generates confusion because goto allows unexpected behaviour in program execution flow.
Your best bet would be to return a Result object with simple bool HasUncachedRecordIDs field and then check it. If it passes, then return. This solution solves the problem of calling a method, which is Any() in this case.
var uncachedRecordIDsResult = LocalCacheController.GetUncachedRecordIDs<PrivateMessage>(messageIDs);
if(uncachedRecordIDsResult.HasUncachedRecordIDs) return;
My reasoning for lack of this feature in the language is that calling GetUncachedRecordIDs in basically any function would unexpectedly end that parent function, without warning. Also, it would intertwine closely both functions, and best programming practices involve loose coupling of classes and methods.
You could pass an Action to your GetUncachedRecordIDs method which you only invoke if you need to. Rough sketch of the idea:
// LocalCacheController
void GetUncachedRecordIDs<T>(List<int> messageIDs, Action<List<int>> action)
{
// ...
if (!cached) {
action(recordIds);
}
}
// ...
public static void CacheUncachedMessageIDs(List<int> messageIDs)
{
LocalCacheController.GetUncachedRecordIDs<PrivateMessage>(messageIDs, uncachedRecordIDs => {
using (var db = new DBContext())
{
// ...
}
});
}

Context message for UPDATE is not working

I am working on a CRM Dynamics Plugin. There is a field on custom entity named "email". I want to make sure that for two entity records email addresses should be unique. For that purpose I have written following code:
public class Class1 : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
// Obtain the execution context from the service provider.
Microsoft.Xrm.Sdk.IPluginExecutionContext context = (Microsoft.Xrm.Sdk.IPluginExecutionContext)
serviceProvider.GetService(typeof(Microsoft.Xrm.Sdk.IPluginExecutionContext));
// Get a reference to the organization service.
IOrganizationServiceFactory factory =
(IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = factory.CreateOrganizationService(context.UserId);
// The InputParameters collection contains all the data passed in the message request.
if (context.InputParameters.Contains("Target") &&
context.InputParameters["Target"] is Entity)
{
Entity entity = (Entity)context.InputParameters["Target"];
//</snippetAccountNumberPlugin2>
// Verify that the target entity represents an account.
// If not, this plug-in was not registered correctly.
if (context.MessageName.ToUpper() == "CREATE")
{
if (entity.LogicalName == "new_assignment1entity")
{
try
{
QueryExpression query = new QueryExpression("new_assignment1entity");
query.ColumnSet.AddColumns("new_email");
EntityCollection result1 = service.RetrieveMultiple(query);
foreach (var a in result1.Entities)
{
int size = result1.Entities.Count;
if (a.Attributes["new_email"].ToString().Equals(entity["new_email"]))
throw new InvalidPluginExecutionException("Duplicate Email found!");
}
}
catch (FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault>)
{
//You can handle an exception here or pass it back to the calling method.
throw new InvalidPluginExecutionException("Some problem occurred while Querying Records!");
}
}
}
else if (context.MessageName.ToUpper() == "UPDATE")
{
if (entity.LogicalName == "new_assignment1entity")
{
try
{
QueryExpression query = new QueryExpression("new_assignment1entity");
query.ColumnSet.AddColumns("new_email");
EntityCollection result1 = service.RetrieveMultiple(query);
foreach (var a in result1.Entities)
{
int size = result1.Entities.Count;
if (a.Attributes["new_email"].ToString().Equals(entity["new_email"]))
throw new InvalidPluginExecutionException("Duplicate Email found!");
}
}
catch (FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault>)
{
//You can handle an exception here or pass it back to the calling method.
throw new InvalidPluginExecutionException("Some problem occurred while Querying Records!");
}
}
}
}
}
}
When User creates a new entity record with duplicate email address this code works and shows a dialog box printing error message. But when User edit an existing record (update and existing record) and makes the email address duplicate then this code does not work and updated record with duplicated email address saved.
I am guessing that Context message with UPDATE else part is not working.
Please help me out.
It's not really worth trying to debug this as unfortunately you are going about this in a horribly inefficient way. (Though the most likely cause is the way you are querying being subject to a "feature" of CRM which means you are not querying all the records you think you are).
In short, your code says:
Get me ALL(*) instances of the new_assignment1entity entity
Look at each record until I find one with an email address that matches (case-sensitive) the value just provided in the update
Throw an exception when you encounter the first exact match (otherwise continue with the transaction)
Mains points of note:
QueryExpression will only return the maximum first 5000 records in CRM
You should be filtering your query to only return new_assignment1entity records where the new_email attribute matches the provided value
String.Equals(string) is case-sensitive so to truly check for a duplicate, you should convert the case of each value
Your size variable serves no purpose
Your code will throw an exception if the new/updated record has no value for new_email. You should check that the attribute exists before attempting to access it
I resolved this issue. The problem why only Create execution flow was running and not Update is that I had only registered the plugin for create message step. To overcome this issue, I added a new step in same plugin and registered it with update message as show in following screenshot:
And it worked like charm.
Apart from this, #GregOwens mentioned very helpful points.These should follow as best practices in CRM Development.

Categories

Resources