How to use Created (or CreatedAtAction / CreatedAtRoute) in an asp net core api

CreatedAtAction gives the best output in my opinion. The following controller code will do what you need:

[Route("api/products")]
[ApiController]
public class ProductsController : ControllerBase
{
    private readonly IProductRepository productRepository;

    public ProductsController(IProductRepository productRepository)
    {
        this.productRepository = productRepository;
    }

    [HttpPost]
    [Route("")]
    [ProducesResponseType(StatusCodes.Status201Created)]
    public ActionResult<Product> CreateProduct(ProductCreateDto product)
    {
        if (product is null)
            return BadRequest(new ArgumentNullException());

        var entity = productRepository.CreateProduct(product);

        return CreatedAtAction(nameof(GetProduct), new { id = entity.ID }, entity);
    }

    [HttpGet]
    [Route("{id}")]
    public ActionResult<Product> GetProduct(int id)
    {
        return productRepository.GetProduct(id);
    }
}

Issuing the following request:

POST http://localhost:5000/api/products HTTP/1.1 
Host: localhost:5000
Connection: keep-alive 
Content-Length: 25 
Content-Type: application/json

{ "name": "ACME Widget" }

Will yield the following response:

HTTP/1.1 201 Created
Date: Mon, 12 Oct 2020 09:50:00 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 29
Location: http://localhost:5000/api/products/1

{"id":1,"name":"ACME Widget"}

In your route for the Get method, take both the leading / and the $ out (i.e. it should just be "{id}"). Having the leading / in there means that the route will be relative to the base of the application; taking it out makes the route for the method relative to the controller's base path instead. The $ is being treated as a literal character in the route, hence why it was appearing in the Location header in Attempt 1. Once you've made the changes, you should find that your CreatedAtRoute call works as you would expect.