One Table, Several Views, one C# Class - c#

I have a single c# class defined as
class HighScore
{
public int Id { get; set; }
[DataMember(Name = "PlayerName")]
public string PlayerName { get; set; }
[DataMember(Name = "PlayerCountry")]
public int PlayerCountry { get; set; }
[DataMember(Name = "PlayerTime")]
public double PlayerTime { get; set; }
[DataMember(Name = "PlayerBadge")]
public int PlayerBadge { get; set; }
}
And a table in my Azure mobile services SQL database that contains several records of this type. I have a number of views
select * from tellingthetime.HighScore where PlayerBadge=0
where the PlayerBadge is a number from 0 to 4. I also have a number of read scripts added to my Mobile Service that query the view and return the appropriate rows.
function read(query, user, request) {
mssql.query("select * from OneStarBadgeLeaderBoard", {
success: function(results) {
console.log(results);
request.respond(statusCodes.OK, results);
}
});
}
The above script is called OneStarBadgeLeaderBoard, but my class is called HighScore. The code below I call to Get the underlying table.
private IMobileServiceTable<HighScore> HighScoreTable = App.MobileService.GetTable<HighScore>();
Without creating a different class name, the defintions are all the same as the SQL returned data is the same, for each read script, how do I make this work so I can call any read script, which queries the appropriate view to retrieve the values I need?
Hope that make sense.
Many thanks,
Jason.
P.S. Of course, I could read the entire table and query it with LINQ on the client, but that will increase the amount of data to download.

Got this from Josh Twist
function read(query, user, request) {
var dispatch = {
op1 : operation1,
op2 : operation2,
}
if (request.parameters.operation && dispatch.hasOwnProperty(request.parameters.operation)) {
dispatch[request.parameters.operation](query, user, request);
return;
}
else
{
// default path for execution
request.execute();
}
}
function operation1(query, user, request) {
request.respond(200, "this result is from operation1");
}
function operation2(query, user, request) {
request.respond(200, "this result is from operation2");
}
http://www.thejoyofcode.com/Dispatching_to_different_query_functions_in_Mobile_Services.aspx
Also this code sends a filtered OData request that only retrieves the required records. Got it from the ToDo list Azure Mobile Service Tutorials
private async void RefreshTodoItems()
{
// This code refreshes the entries in the list view by querying the TodoItems table.
// The query excludes completed TodoItems
var results = await todoTable
.Where(todoItem => todoItem.Complete == false)
.ToListAsync();
items = new ObservableCollection<TodoItem>(results);
ListItems.ItemsSource = items;
}

Related

Handling Stripes payment_intent.succeeded Webhook if it competes with a post back from the client to create an entity in the DB

I need some advice on the workflow for my application when charging a credit card using Stripe.
Scenario 1 - I don't use any webhook for payment_intent.succeeded so when I call stripe.confirmCardPayment on the client side in Javascript
and receive the paymentIntent back I then post to my server and create an entry in a "Payment" table with some method called "SavePayment()", where all the details (card id, exp month, amount, etc) will be stored. Once I save to the DB, I can return the details to the client (points earned, payment successful message, etc). Then we're done!
Scenario 2 Client(user) closes the browser after Stripe is called to charge the card, but before it can post back to my server to add the "Payment" entity. So now I use a webhook for payment_intent.succeeded as others have recommended doing this for redundancy.
Problem -
Because the webhook is triggered immediately, after the card is charged by Stripe, my server could potentially receive two different entry points (client posting back to server to save a payment and Stripes webhook trigger event), to create a "Payment" entity in my DB.
Now this isn't a huge problem, because both entry points can query for the "Payment" entity based on it's unique identifier (PaymentIntentId) to see if it exists in the DB.
But let's say both entry points query and return a null, so now both entry points go ahead and create a new "Payment" entity and attempt to save it in the DB. One will succeed and one will now fail, frequently creating a unique identifier constraint exception being thrown by SQL Server.
Solution? - This doesn't seem like the ideal workflow/scenario, where multiple exceptions could be frequently thrown, for creating an entity in my DB. Is there a better workflow for this, or am I stuck implementing it this way?
Here is some of my code/suedo code to look at.
public class Payment : BaseEntity
{
public string PaymentIntentId { get; set; }
public int Amount { get; set; }
public string Currency { get; set; }
public string CardBrand { get; set; }
public string CardExpMonth { get; set; }
public string CardExpYear { get; set; }
public int CardFingerPrint { get; set; }
public string CardLastFour { get; set; }
public PaymentStatus Status { get; set; }
public int StripeFee { get; set; }
public int PointsAwarded { get; set; }
public int PointsBefore { get; set; }
public int PointsAfter { get; set; }
public string StripeCustomer { get; set; }
public int UserId { get; set; }
public User User { get; set; }
}
Here is some code from the client to call stripe and then post to my server
// submit button is pressed
// do some work here then call Stripe
from(this.stripe.confirmCardPayment(this.paymentIntent.clientSecret, data)).subscribe((result: any) => {
if (result.paymentIntent) {
let payment = {
paymentIntentId: result.paymentIntent.id,
amount: result.paymentIntent.amount,
currency: result.paymentIntent.currency,
// fill in other fields
};
this.accountService.savePayment(payment).subscribe(response => {
if (response.status === 'Success') {
// do some stuff here
this.alertService.success("You're purchase was successful");
this.router.navigateByUrl('/somepage');
}
if (response.status === 'Failed') {
this.alertService.danger("Failed to process card");
}
}, error => {
console.log(error);
this.alertService.danger("Oh no! Something happened, please contact the help desk.");
}).add(() => {
this.loadingPayment = false;
});
} else {
this.loadingPayment = false;
this.alertService.danger(result.error.message);
}
});
Here is the server controller to save a "Payment" entity
[HttpPost("savepayment")]
public async Task<ActionResult> SavePayment(StripePaymentDto paymentDto)
{
var userFromRepo = await _userManager.FindByEmailFromClaimsPrinciple(HttpContext.User);
if (userFromRepo == null)
return Unauthorized(new ApiResponse(401));
// this calls the Stripe API to get the PaymentIntent (just incase the client changed it)
var paymentIntent = await _paymentService.RetrievePaymentIntent(paymentDto.PaymentIntentId);
if (paymentIntent == null) return BadRequest(new ApiResponse(400, "Problem Retrieving Payment Intent"));
var payment = _mapper.Map<StripePaymentDto, StripePayment>(paymentDto);
payment.UserId = userFromRepo.Id;
if (paymentIntent.Status == "succeeded") {
// fill in all the necessary fields
// left out for brevity
} else if (paymentIntent.Status == "requires_payment_method") {
payment.Status = PaymentStatus.Failed;
_logger.LogInformation("Payment Intent is not successful. Status: " + paymentIntent.Status + " PaymentIntentId: " + paymentIntent.PaymentIntentId);
// send payment failure email
} else {
// don't know if this will be needed
payment.Status = PaymentStatus.Pending;
}
_unitOfWork.Repository<StripePayment>().Add(payment);
var success = await _unitOfWork.Complete();
if (success > 0) {
if (payment.Status == PaymentStatus.Success) {
// send email
}
return Ok(_mapper.Map<StripePayment, StripePaymentDto>(payment));
}
return BadRequest(new ApiResponse(400, "Failed to save payment"));
}
Here is the Stripe webhook
[HttpPost("webhook")]
public async Task<ActionResult> StripeWebhook()
{
var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();
// if this doesn't match we get an exception (sig with whSec)
var stripeEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"], _whSecret);
PaymentIntent intent;
switch (stripeEvent.Type)
{
case "payment_intent.succeeded":
intent = (PaymentIntent)stripeEvent.Data.Object;
_logger.LogInformation("Payment Succeeded: ", intent.Id);
this.ProcessSuccess(intent);
// order = await _paymentService.UpdateOrderPaymentSucceeded(intent.Id);
// _logger.LogInformation("Order updated to payment received: ", order.Id);
break;
case "payment_intent.payment_failed":
intent = (PaymentIntent)stripeEvent.Data.Object;
_logger.LogInformation("Payment Failed: ", intent.Id);
// _logger.LogInformation("Payment Failed: ", order.Id);
break;
}
return new EmptyResult();
}
private async void ProcessSuccess(PaymentIntent paymentIntent) {
var spec = new PaymentsWithTypeSpecification(paymentIntent.Id);
var paymentFromRepo = await _unitOfWork.Repository<StripePayment>().GetEntityWithSpec(spec);
if (paymentFromRepo == null) {
// create one and add it
var payment = _mapper.Map<PaymentIntent, StripePayment>(paymentIntent);
payment.UserId = Convert.ToInt32(paymentIntent.Metadata["userid"]);
}
// finish work here and then save to DB
}
Great point below. I appreciate your goal. After some thought, my final analysis is that: in order to prevent duplicate records in the database from multiple sources, a unique index should be used. (which you are using)
Now by using a unique index the database will throw an exception which the code will have to handle gracefully. Hence the answer is that you are doing it the way I and others have done so for some years. Unfortunately, I'm not aware of any other means of avoiding an exception once you hit the database tier.
Great question even if the answer is not the one you were hoping for.

How do I correctly use the SendBulkTemplatedEmailRequest from SES in the AWS-SDK-NET?

I am attempting to use the AmazonSimpleEmailService client via the AWS-SDK for .Net, to send a SendBulkTempatedEmailRequest. I have implemented a dedicated handler for actually building the request and making the SendBulkTemplatedEmailAsync call. It is not working as I expect. I think there is a bug with how the request object is serialized and passed to the API.
Here is some sample code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Amazon.SimpleEmail;
using Amazon.SimpleEmail.Model;
using Newtonsoft.Json;
namespace Sample.AWS.SES
{
public class SendEmailService
{
private readonly IAmazonSimpleEmailService _sesClient;
public SendEmailService(IAmazonSimpleEmailService sesClient)
{
_sesClient = sesClient;
}
public async Task<string> SendBulkEmailAsync(SesOrderCreatedBulkTemplate data)
{
var result = string.Empty;
var request = new SendBulkTemplatedEmailRequest
{
Template = data.Template,
ConfigurationSetName = data.ConfigurationSet,
DefaultTemplateData = JsonConvert.SerializeObject(data.DefaultTemplateData),
Source = data.Source,
Destinations = data.Destinations
.Select(d => new BulkEmailDestination
{
Destination = new Destination
{
ToAddresses = d.ToAddresses.ToList(),
},
ReplacementTemplateData = string.Empty
})
.ToList(),
ReplyToAddresses = data.ReplyToAddresses.ToList()
};
try
{
var resp = await _sesClient.SendBulkTemplatedEmailAsync(request);
}
catch (Exception ex)
{
var msgEx = new Exception("Error sending message to SES.", ex);
throw msgEx;
}
return result;
}
public class SesOrderCreatedBulkTemplate
{
public string Source { get; set; }
public string Template { get; set; }
public string ConfigurationSet { get; set; }
public IEnumerable<Destination> Destinations { get; set; }
public MyTemplateData DefaultTemplateData { get; set; }
public IEnumerable<string> ReplyToAddresses { get; set; }
public string ReturnPath { get; set; } = string.Empty;
}
public class DestinationObj
{
public IEnumerable<string> ToAddresses { get; set; }
public MyTemplateData ReplacementTemplateData { get; set; }
public DestinationObj() {}
}
public class MyTemplateData
{
public List<Person> Tenants { get; set; }
}
public class Person
{
public string PersonName { get; set; }
public List<object> PersonData { get; set; }
}
}
}
The properties for SourceArn, TemplateArn and ReturnPathArn are omitted on purpose. According the SES documentation, the SDK wraps the low-level functionality of the Amazon SES API with higher-level data types and function calls that take care of the details for you. When I view the API documentation for sending bulk email, the ARN properties are all list as not required. When I look at the some CLI examples, it is the same. When I look at the documentation for the SDK for .Net v3, it is ambiguous (not marked as required or optional).
Because the SDK supposed to wrap the low-level functionality of the API, I do not believe the ARN values are required (neither the API nor the CLI require them). However, when I attempt to actually use the request object created in the code snippet, I get an error that says InvalidTemplateData.
If I serialize the request object to JSON, then remove the 3 ARN fields from the string, I can use either the API or the CLI to successfully send the message.
In addition to not specifying a value for the ARN's, I have tried (for all 3 ARN values):
specificArn = string.empty;
specificArn = new {};
specificArn = "";
I have also tried explicitly newing-up the object separate from initializing the properties:
var request = new SendBulkTemplatedEmailRequest();, and then individually populating the properties.
If I don't initialize the ARN values, I get an error about NoneType vs StringType when the send method is called. The variations on string initialization that I tried result in InvalidTemplateData errors.
Note, I do know ARN values for Source and ReturnPath. I do not have an ARN value for the template we use. Supposedly, using the CLI, when you create a template you should receive a response back that includes the ARN for the template. I get no response from the CLI when I create a template, but it does get created every time I try. The describe-template CLI command is not valid when you specify SES and responds with an error if I don't specify the workspace (whatever you call the SES space) value.
Does anyone have a suggestion on how to solve this?
From the provided code it's hard to say what you pass into API.
This is how I send bulk emails:
SES configuration
create a template (taken from AWS SES docs) and save it to a file - my-template.json
{
"Template": {
"TemplateName": "my-template",
"SubjectPart": "Greetings, {{name}}!",
"HtmlPart": "<h1>Hello {{name}},</h1><p>Your favorite animal is {{favoriteanimal}}.</p>",
"TextPart": "Dear {{name}},\r\nYour favorite animal is {{favoriteanimal}}."
}
}
create a template via CLI aws ses create-template --cli-input-json file://my-template.json
SES .NET SDK API
async Task SendAsync(string[] receivers)
{
var destinations = receivers
.Select(receiver => new BulkEmailDestination
{
Destination = new Destination(new List<string> { receiver }),
ReplacementTemplateData = ToJson(receiver, "Doggy")
})
.ToList();
var bulkTemplate = new SendBulkTemplatedEmailRequest
{
Source = "your-email#gmail.com", // your email you bulk send from
Template = "my-template", // your template name
DefaultTemplateData = ToJson("<not set>", "<not set>"),
Destinations = destinations
};
await _client.SendBulkTemplatedEmailAsync(bulkTemplate);
}
// Create replacement data by serializing Dictionary
string ToJson(string name, string favoriteanimal)
=> JsonSerializer.Serialize(new Dictionary<string, string>
{
{ "name", name },
{ "favoriteanimal", favoriteanimal }
});

Mongodb query to match based on 2 fields

Let me give a example first,
{
$match: {
$or:[
{'sender':1, 'recipient':2},
{'sender':2, 'recipient':1},
{'sender':1, 'recipient':3},
{'sender':7, 'recipient':2},
{'sender':7, 'recipient':3} //goes on may be 20 or 30
]
}
}
I am trying to fetch data based on the sender and the recipient. If the sender and recipient falls in this combination of categories, I pick only that data.
From the above example I can say that the combination of sender:7 and recipient:1 is not valid, only the combination of sender:7 and recipient:2 or recipient:3 is valid.
Is there any way to simplify the above mentioned query in C#?
If you want to write c# queries i would suggest using a repository for this it would make things much easier for you and you can write some entity framework style queries
https://github.com/alexandre-spieser/mongodb-generic-repository
public class TestMongoRepository: BaseMongoRepository, IEmailMongoRepository
{
public TestMongoRepository(string connectionString, string databaseName) : base(connectionString, databaseName)
{
}
//public MongoRepository<T> Create<T>() where T:IEntity
//{
// return new MongoRepository<T>();
//}
}
}
public class Data: Document
{
public string Sender { get; set; }
public string Receiver{ get; set; }
}
then you could query using the following syntaxt
var _yourRepository=new TestMongoRepository("connectionstring","database");
class Combination{
public int Sender{get;set;}
public int Receiver {get;set;}
}
var combinations=new List<Combination>{
new Combination{Sender=1, Receiver=5},
// add your other comibnaitons here
}
var data= _yourRepository.GetAll<Data>(e =>combinations.Any(c=>c.Sender=e.Sender && c.Receiver=e.Receiver) );

RavenDB - stream index query results in exception

We're currently trying to use the Task<IAsyncEnumerator<StreamResult<T>>> StreamAsync<T>(IQueryable<T> query, CancellationToken token = null), running into some issues.
Our document look something like:
public class Entity
{
public string Id { get; set; }
public DateTime Created { get; set; }
public Geolocation Geolocation { get; set; }
public string Description { get; set; }
public IList<string> SubEntities { get; set; }
public Entity()
{
this.Id = Guid.NewGuid().ToString();
this.Created = DateTime.UtcNow;
}
}
In combination we've a view model, which is also the model were indexing:
public class EntityViewModel
{
public string Id { get; set; }
public DateTime Created { get; set; }
public Geolocation Geolocation { get; set; }
public string Description { get; set; }
public IList<SubEntity> SubEntities { get; set; }
}
And ofcourse, the index, with the resulttype inheriting from the viewmodel, to enable that SubEntities are mapped and output correctly, while enabling the addition of searchfeatures such as fulltext etc.:
public class EntityWithSubentitiesIndex : AbstractIndexCreationTask<Entity, EntityWithSubentitiesIndex.Result>
{
public class Result : EntityViewModel
{
public string Fulltext { get; set; }
}
public EntityWithSubentitiesIndex ()
{
Map = entities => from entity in entities
select new
{
Id = entity.Id,
Created = entity.Created,
Geolocation = entity.Geolocation,
SubEntities = entity.SubEntities.Select(x => LoadDocument<SubEntity>(x)),
Fulltext = new[]
{
entity.Description
}.Concat(entity.SubEntities.Select(x => LoadDocument<SubEntity>(x).Name)),
__ = SpatialGenerate("__geolokation", entity.Geolocation.Lat, entity.Geolocation.Lon)
};
Index(x => x.Created.Date, FieldIndexing.Analyzed);
Index(x => x.Fulltext, FieldIndexing.Analyzed);
Spatial("__geolokation", x => x.Cartesian.BoundingBoxIndex());
}
}
Finally we're querying like this:
var query = _ravenSession.Query<EntityWithSubentitiesIndex.Result, EntityWithSubentitiesIndex>()
.Customize(c =>
{
if (filter.Boundary == null) return;
var wkt = filter.Boundary.GenerateWkt().Result;
if (!string.IsNullOrWhiteSpace(wkt))
{
c.RelatesToShape("__geolokation", wkt, SpatialRelation.Within);
}
})
.AsQueryable();
// (...) and several other filters here, removed for clarity
var enumerator = await _ravenSession.Advanced.StreamAsync(query);
var list = new List<EntityViewModel>();
while (await enumerator.MoveNextAsync())
{
list.Add(enumerator.Current.Document);
}
When doing so we're getting the following exception:
System.InvalidOperationException: The query results type is 'Entity'
but you expected to get results of type 'Result'. If you want to
return a projection, you should use
.ProjectFromIndexFieldsInto() (for Query) or
.SelectFields() (for DocumentQuery) before calling to
.ToList().
According to the documentation, the Streaming API should support streaming via an index, and querying via an IQueryable at once.
How can this be fixed, while still using an index, and the streaming API, to:
Prevent having to page through the normal query, to work around the default pagesize
Prevent having to load the subentities one at a time when querying
Thanks in advance!
Try to use:
.As<Entity>()
(or .OfType<Entity>()) in your query. That should work in the regular stream.
This is a simple streaming query using "TestIndex" that is an index over an entity Test and I'm using a TestIndex.Result to look like your query. Note that this is actually not what the query will return, it's only there so you can write typed queries (ie. .Where(x => x.SomethingMapped == something))
var queryable = session.Query<TestIndex.Result, TestIndex>()
.Customize(c =>
{
//do stuff
})
.As<Test>();
var enumerator = session.Advanced.Stream(queryable);
while (enumerator.MoveNext())
{
var entity = enumerator.Current.Document;
}
If you instead want to retrieve the values from the index and not the actual entity being indexed you have to store those as fields and then project them into a "view model" that matches your mapped properties. This can be done by using .ProjectFromIndexFieldsInto<T>() in your query. All the stored fields from the index will be mapped to the model you specify.
Hope this helps (and makes sense)!
Edit: Updated with a, for me, working example of the Streaming API used with ProjectFromIndexFieldsInto<T>() that returns more than 128 records.
using (var session = store.OpenAsyncSession())
{
var queryable = session.Query<Customers_ByName.QueryModel, Customers_ByName>()
.Customize(c =>
{
//just to do some customization to look more like OP's query
c.RandomOrdering();
})
.ProjectFromIndexFieldsInto<CustomerViewModel>();
var enumerator = await session.Advanced.StreamAsync(queryable);
var customerViewModels = new List<CustomerViewModel>();
while (await enumerator.MoveNextAsync())
{
customerViewModels.Add(enumerator.Current.Document);
}
Console.WriteLine(customerViewModels.Count); //in my case 504
}
The above code works great for me. The index has one property mapped (name) and that property is stored. This is running the latest stable build (3.0.3800).
As #nicolai-heilbuth stated in the comments to #jens-pettersson's answer, it seems to be a bug in the RavenDB client libraries from version 3 onwards.
Bug report filed here: http://issues.hibernatingrhinos.com/issue/RavenDB-3916

Binding RadGridView with stored procedure's result in EntityFramework

I have stored the result of a stored procedure (in Entity Framework) in an IList and then bind my grid with this IList. When this result is null the grid hasn't got any columns but I need to show these columns in the grid. Is there any way to solve this problem?
This is my code:
IList list = new ArrayList();
try
{
var context = new SabzNegar01Entities1();
list = (from p in context.tbl_ReturnSalesFactor_D
let add = (p.MainNum * p.Fee)
let pureAdd = ((p.MainNum * p.Fee) - (p.MainNum * p.Discount)) + ((p.Tax + p.Charges) * p.MainNum)
let taxChange = (p.Tax + p.Charges) * p.MainNum
let discount = p.Discount * p.MainNum
where p.DocNo == inDocNo
select new { p.Row, p.StockCode, p.tbl_Stock.PDescription, p.Fee, p.MainNum, add, taxChange, discount, pureAdd }).ToList();
}
catch (Exception ex)
{
PMessageBox.Show(ex.Message, "Error in Reading ReturnSalesFactor_Details Data");
}
and binding:
radGridView_Product.DataSource = list ;
I would do this:
define a C# class that matches the data that you're getting back from the stored procedure (e.g. SalesInfo or whatever you want to call it)
then define your IList to be a List<SalesInfo> (please don't use the crappy old ArrayList anymore!)
when you call the stored procedure, but you get no values back, you just add a dummy SalesInfo entry to your list being returned, that e.g. has no data found as its description and everything else is empty/0.0
That way, your method will always return at least one element, and since that element is there, the gridview know it's columns and what to call them
Update:
I would first define a class to hold all those properties you want to display in your gridview:
// just a data container to hold the information - call it whatever you like!
// also: with the datatypes, I am just *GUESSING* because you didn't exactly tell us
// what those values are - adapt as needed !
public class SalesInfo
{
public int Row { get; set; }
public string StockCode { get; set; }
public string Description { get; set; }
public decimal Fee { get; set; }
public decimal MainNum { get; set; }
public decimal Add { get; set; }
public decimal TaxChange { get; set; }
public decimal Discount { get; set; }
public decimal PureAdd { get; set; }
}
Next, define a method that goes and gets that data from the stored procedure - if not data is returned, add a dummy entry instead:
// Define a method to return an IList of that data container class defined above
public IList<SalesInfo> GetSalesInfo()
{
// please, as of .NET 2.0 - use the List<T> and stop using ArrayList!
IList<SalesInfo> list = new List<SalesInfo>();
try
{
// put your context usage into using()..... blocks to ensure proper disposal
using (var context = new SabzNegar01Entities1())
{
// fetch the data, turn it into SalesInfo records
list = (from p in context.tbl_ReturnSalesFactor_D
where p.DocNo == inDocNo
select new SalesInfo
{
Row = p.Row,
StockCode = p.StockCode,
Description = p.tbl_Stock.PDescription,
Fee = p.Fee,
MainNum = p.MainNum,
Add = p.MainNum*p.Fee,
PureAdd = ((p.MainNum*p.Fee) - (p.MainNum*p.Discount)) + ((p.Tax + p.Charges)*p.MainNum),
Discount = p.Discount*p.MainNum,
TaxChange = (p.Tax + p.Charges)*p.MainNum
}).ToList();
}
// if you get back no data -> add a dummy entry
if (list.Count <= 0)
{
list.Add(new SalesInfo { Description = "(no data found)" });
}
}
catch (Exception ex)
{
PMessageBox.Show(ex.Message, "Error in Reading ReturnSalesFactor_Details Data");
}
// return the resulting list
return list;
}

Categories

Resources