2

Let's say I have the following model:

public class Product
{        
    public string Id { get; set; }
    public string Name { get; set; }
    public string Category { get; set; }
    public decimal Price { get; set; }
}

Now, I'd really like to allow people to add to and view my catalog of products. So, I create the following controller:

Note: Please don't be too grossed out by the static list. I know it's wrong.

public class ProductsController : ApiController
{
    static readonly List<Product> products = new List<Product>();

    // ...

    [HttpPost]
    public void AddProduct(Product value)
    {
        products.Add(product);
    }
}

As you may have noticed, I'm currently allowing people to add new products with custom Id values. This doesn't feel right to me. I should be in control of my products' identifiers, right? I don't want to give people the idea they can set them.

My attempted solution was to create a DTO for my product model, then to install the AutoMapper package.

Here's the DTO:

public class ProductDto
{
    public string Name { get; set; }
    public string Category { get; set; }
    public decimal Price { get; set; }
}

And here's the new controller:

public class ProductsController : ApiController
{
    static readonly List<Product> products = new List<Product>();

    // ...

    [HttpPost]
    public void AddProduct(ProductDto value)
    {
        Product product = Mapper.Map<Product>(value);
        product.Id = Guid.NewGuid().ToString();

        products.Add(product);
    }
}

Is this reasonable? Is there a simpler, more canonical approach?

2

Actually... back in the day (and still today) it was very common to not give people the ability to generate ids on the client side for one simple reason: the ids were auto incremented integer values and were generated by the database engine.

But there are problems with this approach:

  1. you are tying yourself to the database implementation,
  2. auto-generated ids are not secure at all and are prone to id guessing - a technique where you can guess how many resources are in your databases by incrementing the id by one until you receive not found response.

People tried to fix the second problem by using some id obfuscation and/or finding a different way to identify a resource within the system and lately it has become more and more popular to use Guids/Uuids instead.

But then came the apparent question: If the ids are no longer generated by the database engine and tied to backend and Uuids V4 are virtually unique (the risk of collision is pretty much non-existent for any regular size project), why do we even need the server to generate them?

And the answer was: You do not.


Ok, to be honest, if your entire system runs on-line and cannot work without a server then perhaps still having the server to generate the Uuid is fine. But single page and mobile applications backed by REST APIs for cross-platform synchronization have become more and more popular. Having the client generate the Uuids and thus being able to instantly use them (also for cross-resource referencing) makes your application very usable even when it is offline.

You may object that you can make an offline working application with regular ids as well, effectively generating custom local ids and synchronize them with ids on the server. The thing is, with Uuids you can completely skip this synchronization and mapping and treat your local as if it were on the server already, because the ids will remain the same.

So if you think you have to be in control of your resource identification, you do not.

  • 1
    Allowing the client to generate UUIDs will allow malicious clients to cause ID collisions, right? – Tanner Swett Aug 9 '17 at 19:28
  • 2
    You should. Never trust in what comes from outside of your boundaries. – Laiv Aug 9 '17 at 19:36
  • 1
    @DavidPacker would not be easier just ignore then incoming id? The addProduct could set the new UUID regardless what was informed from the client-side – Laiv Aug 9 '17 at 19:38
  • 2
    If the client generates resource URL's the HTTP PUT method is a better choice over POST. – Kwebble Aug 9 '17 at 19:43
  • 1
    @kylemart You can and should check for collisions, yes. As Laiv has said, do not trust user input. A simple unique key for the id should be enough to prevent any problems. – Andy Aug 9 '17 at 19:45
2

Couple points here:

  1. You should own your own, internal, unique identifier, which should not be exposed via the API, neither for adding or viewing. The identifier is for your database to create key relationships. The identifier should be generated by your system (e.g. using an IDENTITY column) as requests to add products are received.

  2. The end user should be able to submit products with a CompanyID (perhaps derived from their signon context) plus a company-specific product identifier such as SKU or UPC code. That should be their only reference number. It's their responsibility to ensure these identifiers don't collide with any other products within that company ID. From your system's perspective, this field might be called ExternalProductID and would form a unique key only when combined with CompanyID.

  3. If I were you I wouldn't include price in the product. Price is a property of an offer for a product. While products are forever, offers come and go (when price changes). If you don't do it this way you'll have issues with updates and product ID re-use.

  • Thanks for the insight! However, I'm not actually building a commerce application. The example I gave in the OP was arbitrarily chosen to illustrate my question. – kylemart Aug 10 '17 at 20:07
1

I am going to assume here that the product IDs are not actually required to be used by the users and it's an internal thing used in your API so it won't matter what it is.

1) Create your product ID using a sequence or whatever as is normal. Don't let the clients choose their IDs.

2) Whenever you expose your catalogue or a product ID for any reason, encrypt (not hash) the product ID using the login ID of the user + some salt or other key. If you don't need the client to refer to product ID between sessions then you can use the session ID.

3) When someone adds a new product, you add it to your DB and create a new ID using the sequence. Return the encrypted one back to the client for continued use.

4) When receiving a product ID from the client, decrypt as appropriate.

5) Internally only ever use your identity keys.

The benefit of this is that

a) You have a unique key that can't be chosen again.

b) Clients effectively can't guess a key

c) Even if someone discovers someone else's version it won't work outside their login (or session depending on what you decide).

d) You have a "normal" database at the back end.

1

Having thought of this for quite some time, I have now come to the same conclusion as the user @Andy.

I'm assuming that your Products API would be at least authenticated and authorised. That should prevent rogue clients from creating random product objects in your database.

The question now is, whether you should handle the task of generating the ids on the server side. I'm going to go ahead and claim that it's not needed.

You're probably going to have to return the identifier back to the client since it's (probably) going to be your primary key in the database. Your client may query about the product it created and using the product name to dig through your entire database might cause a performance hit. Since you're sending the id back to the client, you might as well let it generate it.

The DTO pattern on the other hand requires you to maintain separate classes to practically do the same thing. This goes against all the intuitions of Object Oriented Programming.

To prevent the clients from being able to guess the number of rows in your database, I'd suggest you use a GUID to identify your product objects. GUIDs are reasonably unique for most purposes and are generatable from most languages.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.