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:
ArgoDocumentStore
as singletonIArgoDocumentStore
as singletonIArgoDocumentSession
as scopedIArgoQueryDocumentSession
as 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
Update
method 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());
}