Getting Started with ASP .NET Core
Please, if you didn't already, read Getting started with console applications. Reading it is recommended before reading this page. It will explain types, interfaces and some methods provided by ArgoStore.
This guide is for integrating ArgoStore into web APIs and general web applications using ASP .NET Core framework. In this guide we are going to create new web API and a controller that will be able to create, update, delete, get, query and upsert DB documents.
Create Application
Create new directory
mkdir AspNetCoreExample
Navigate to new directory
cd AspNetCoreExample
Create application
dotnet new webapi --use-program-main --no-https
Add ArgoStore nuget package
dotnet add package ArgoStore.Extensions.DependencyInjection
TIP
In this guide we are using ArgoStore.Extensions.DependencyInjection instead of ArgoStore package. DI package is referencing ArgoStore package and adds support for dependency injection.
Create Document model
Add new file Models/Person.cs
public class Person
{
public Guid Id { get; set; }
public string Name { get; set; } = "";
public int CookiesCount { get; set; }
public string[] Roles { get; set; } = Array.Empty<string>();
}
Property Id will be used for identity. To understand more about identity see Identity page.
Configure ArgoStore
Edit appsettings.json and add connection string
{
"ConnectionStrings": {
"db": "Data Source=c:\\temp\\mywebapp.sqlite"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
TIP
Whole code for this example is available in GitHub repo.
Edit Program.cs and register ArgoStore before var app = builder.Build(); line.
string dbConnectionString = builder.Configuration.GetConnectionString("db")
?? throw new InvalidOperationException("`ConnectionStrings:db` not set");
builder.Services.AddArgoStore(c =>
{
c.ConnectionString(dbConnectionString);
c.RegisterDocument<Person>();
});
In the snipped above, code is getting connection string from configuration JSON file and registering ArgoStore with it. Person is also registered as supported document type.
If you wanted to register another document type outside Program.cs you can call IArgoDocumentStore.RegisterDocument<T>() or ArgoDocumentStore.RegisterDocument<T>().
WARNING
- ArgoStore intentionally does not support operations on non registered documents.
Calling builder.Services.AddArgoStore will register following types within IoC container:
ArgoDocumentStoreas singletonIArgoDocumentStoreas singletonIArgoDocumentSessionas scopedIArgoQueryDocumentSessionas transient
TIP
Sessions can also be opened by calling one of the following methods or overrides:
IArgoDocumentStore.OpenSession()IArgoDocumentStore.OpenQuerySession()ArgoDocumentStore.OpenSession()ArgoDocumentStore.OpenQuerySession()
Create Controller
For your convenience Postman collection for this API can be found here.
Create new controller in Controllers/PersonController.cs file and inject IArgoDocumentSession
[ApiController]
[Route("/api/[controller]")]
public class PersonController : ControllerBase
{
private readonly IArgoDocumentSession _session;
public PersonController(IArgoDocumentSession session)
{
_session = session;
}
}
Create POST and GET Actions
Create POST action in the controller:
[HttpPost]
public IActionResult CreatePerson([FromBody] Person person)
{
_session.Insert(person);
_session.SaveChanges();
return Created($"/api/person/{person.Id}", person);
}
Create GET action to retrieve person by id:
[HttpGet, Route("{id}")]
public IActionResult GetPersonById([FromRoute] Guid id)
{
Person? person = _session.GetById<Person>(id);
if (person == null) return NotFound();
return Ok(person);
}
We can now test our actions with postman or similar REST tool. But first run the application:
dotnet run
Notice the URL used by application, we are going to need it to call our API.
Calling POST http://localhost:5034/api/person with following JSON body:
{
"name": "Tom Doe",
"cookiesCount": 3,
"roles": ["admin", "sales"]
}
Should give response:
{
"id": "76a2c0e4-641c-43f1-8cff-59e98195f03d",
"name": "Tom Doe",
"cookiesCount": 3,
"roles": [
"admin",
"sales"
]
}
TIP
- Port will be different in your application, please check
Properties\launchSettings.json - Id of your model will be different
Copy the id so we can use it to call GET action.
Calling GET http://localhost:5034/api/person/::id should return:
{
"id": "76a2c0e4-641c-43f1-8cff-59e98195f03d",
"name": "Tom Doe",
"cookiesCount": 3,
"roles": [
"admin",
"sales"
]
}
Create DELETE Action
Stop the application and create following action:
[HttpDelete, Route("{id}")]
public IActionResult DeletePersonById([FromRoute] Guid id)
{
Person? person = _session.GetById<Person>(id);
if (person == null) return NotFound();
_session.Delete(person);
_session.SaveChanges();
return NoContent();
}
WARNING
Delete is permanent, ArgoStore does not support soft delete out of the box (at least for now).
This action when called first time with valid Id will return 204 and second time 404.
Alternatively we can call _session.DeleteById<Person>(id):
[HttpDelete, Route("{id}")]
public IActionResult DeletePersonById([FromRoute] Guid id)
{
_session.DeleteById<Person>(id);
_session.SaveChanges();
return NoContent();
}
In this case we are deleting person if exists, if not nothing will happen. In both cases 204 is returned.
Create PUT Update/Upsert Action
ArgoStore supports both update and upsert operations. Upsert is combination of update and insert where insert is performed if document is not found and cannot be updated.
To support both update and upsert in single API endpoint we are going to use x-upsert header, which if set to true will indicate to perform upsert.
Following code is implementing upsert or update logic:
[HttpPut, Route("{id}")]
public IActionResult UpdatePerson(
[FromRoute] Guid id,
[FromBody] Person person,
[FromHeader(Name = "x-upsert")] bool upsert)
{
person.Id = id;
if (id == default) return BadRequest("Id not set");
if (upsert) return Upsert(person);
return Update(person);
}
private IActionResult Update(Person person)
{
throw new NotImplementedException();
}
private IActionResult Upsert(Person person)
{
throw new NotImplementedException();
}
Now we need to implement Update and Upsert methods.
Update
For Update method we are going to get the person and if not found return 404. Otherwise we are going to update document in DB and return 200.
Replace Update method with following:
private IActionResult Update(Person person)
{
Person? dbPerson = _session.GetById<Person>(person.Id);
if (dbPerson == null) return NotFound();
_session.Update(person);
_session.SaveChanges();
return Ok(person);
}
TIP
Updatemethod will call update in DB using key property from provided object.- ArgoStore does not track changed objects like Entity Framework
WARNING
If we don't check if person exists and call Update, on non existing document, no row will be updated. SaveChanges() will not throw exception it's a simple SQL UPDATE call in the background.
Upsert
For upsert we are going to call Upsert method and always return 200.
Replace Upsert method with following:
private IActionResult Upsert(Person person)
{
_session.Upsert(person);
_session.SaveChanges();
return Ok(person);
}
Testing Update and Upsert
INFO
Before we begin with testing this Action please delete db or delete created documents using API.
Create new Person by calling POST with following JSON request body:
{
"id": "b2ffa3af-4ac5-401e-a98c-fd435e71c6c5",
"name": "Thomas Doe",
"cookiesCount": 3,
"roles": [
"admin",
"sales"
]
}
This time we have provided the id and ArgoStore is going to use it as the document key. It won't generate new random value for the key.
If we call update endpoint PUT http://localhost:5034/api/person/b2ffa3af-4ac5-401e-a98c-fd435e71c6c5 with following JSON request body and without any additional headers:
{
"name": "Thomas Doe",
"cookiesCount": 3,
"roles": [
"admin",
"sales"
]
}
We should get response:
{
"id": "b2ffa3af-4ac5-401e-a98c-fd435e71c6c5",
"name": "Thomas Doe",
"cookiesCount": 3,
"roles": [
"admin",
"sales"
]
}
If we try to update non existing person with new random Guid f542964d-5679-4e0b-9043-f90e3832d676 by calling PUT http://localhost:5034/api/person/f542964d-5679-4e0b-9043-f90e3832d676 with following JSON request body and without any additional headers:
{
"name": "Marcus Kovalski",
"cookiesCount": 7,
"roles": []
}
We should get 404. However if we provide x-upsert header set to true we should get following 200 response:
{
"id": "f542964d-5679-4e0b-9043-f90e3832d676",
"name": "Marcus Kovalski",
"cookiesCount": 7,
"roles": []
}
Create GET Query Action
INFO
Before we create Query Action stop the application if running and delete db file.
Run the application and create some sample data by calling POST action with:
{
"name": "Tom Doe",
"cookiesCount": 3,
"roles": ["admin", "sales"]
}
and
{
"name": "Jane Doe",
"cookiesCount": 4,
"roles": ["admin", "management"]
}
We are going to use this documents for testing.
Empty action method:
[HttpGet]
public IActionResult GetPersons(
[FromQuery] string? name,
[FromQuery] string? role,
[FromQuery] int? cookiesCount)
{
throw new NotImplementedException();
}
We want to be able to query persons by name, role and cookiesCount. In case when name is provided we want to filter by name, if role is provided we want to query by role, etc...
In order to build query dynamically we need to use IQueryable<Person> which we can get by calling _session.Query<Person>().
So following code will add query filter on name if name is set.
IQueryable<Person> query = _session.Query<Person>();
if (!string.IsNullOrWhiteSpace(name))
{
query = query.Where(x => x.Name.Contains(name,
StringComparison.OrdinalIgnoreCase));
}
WARNING
_session.Query<T>() is returning IArgoStoreQueryable<T>. Do not user var in this example. Use explicitly IQueryable<T>.
In order to filter on roles add:
if (!string.IsNullOrWhiteSpace(role))
{
query = query.Where(x => x.Roles.Contains(role));
}
Similarly for cookiesCount:
if (cookiesCount.HasValue)
{
query = query.Where(x => x.CookiesCount == cookiesCount);
}
TIP
Query is executed at the end when needed (when writing HTTP response or calling ToList, First, ...), no data is filtered in memory.
Full method looks like this:
[HttpGet]
public IActionResult GetPersons(
[FromQuery] string? name,
[FromQuery] string? role,
[FromQuery] int? cookiesCount)
{
IQueryable<Person> query = _session.Query<Person>();
if (!string.IsNullOrWhiteSpace(name))
{
query = query.Where(x => x.Name.Contains(name,
StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(role))
{
query = query.Where(x => x.Roles.Contains(role));
}
if (cookiesCount.HasValue)
{
query = query.Where(x => x.CookiesCount == cookiesCount.Value);
}
return Ok(query.ToList());
}