ASP NET MVC OutputCache VaryByParam complex objects - c#

This is what I have:
[OutputCache(Duration = 3600, VaryByParam = "model")]
public object Hrs(ReportFilterModel model) {
var result = GetFromDatabase(model);
return result;
}
I want it to cache a new result for each different model. At the moment it is caching the first result and even when the model changes, it returns the same result.
I even tried to override ToString and GetHashCode methods for ReportFilterModel. Actually I have about more properties I want to use for generating unique HashCode or String.
public override string ToString() {
return SiteId.ToString();
}
public override int GetHashCode() {
return SiteId;
}
Any suggestions, how can I get complex objects working with OutputCache?

The VaryByParam value from MSDN: A semicolon-separated list of strings that correspond to query-string values for the GET method, or to parameter values for the POST method.
If you want to vary the output cache by all parameter values, set the attribute to an asterisk (*).
An alternative approach is to make a subclass of the OutputCacheAttribute and user reflection to create the VaryByParam String. Something like this:
public class OutputCacheComplex : OutputCacheAttribute
{
public OutputCacheComplex(Type type)
{
PropertyInfo[] properties = type.GetProperties();
VaryByParam = string.Join(";", properties.Select(p => p.Name).ToList());
Duration = 3600;
}
}
And in the Controller:
[OutputCacheComplex(typeof (ReportFilterModel))]
For more info:
How do I use VaryByParam with multiple parameters?
https://msdn.microsoft.com/en-us/library/system.web.mvc.outputcacheattribute.varybyparam(v=vs.118).aspx

Related

C# losing value between two controller ActionResults

I'm having an issue where a value seems to be being dropped between two controller ActionResults. I'm creating request as a new ValuationRequest and adding 4 values as below.
The WriteLine correctly shows ValuationType as "lettings"
request = new ValuationRequest
{
ValuationType = new SearchType[] { SearchType.lettings },
Postcode = model.Postcode,
FromDate = DateTime.Now.AddHours(24),
ToDate = DateTime.Now.AddDays(14)
};
Debug.WriteLine("ValTypeBefore:" + request.ValuationType[0].ToString());
return RedirectToAction("select-appointment", request);
However, when I pass request through to the next ActionResult shown below, and immediately try to Debug.WriteLine again, it errors as this value is null. The other 3 fields are being carried across perfectly.
[ActionName("select-appointment")]
public ActionResult SelectAppoinment(ValuationRequest request, ValuationModel model)
{
Debug.WriteLine("ValTypeAfter:" + request.ValuationType[0].ToString());
var valuationAppointments = WebServiceUtility.GetValuationAppointments(request);
Any ideas why this would happen?
The 'request' is being passed through, but just ValuationType is being dropped.
Code for ValuationRequest class below:
public partial class ValuationRequest {
private string postcodeField;
private string officeCodeField;
private System.DateTime fromDateField;
private System.DateTime toDateField;
private int durationField;
private bool durationFieldSpecified;
private int interludeField;
private bool interludeFieldSpecified;
private SearchType[] valuationTypeField;
Cheers
It might have to do with JSON Serialization, which only serializes public Properties.
If you redirect, your request will get serialized and then desiralized auto-magically using (usually) JSON. So you will need public Properties with getter and setter for all data you want to transmit. Also an empty Constructor.
But if those Actions are in the same controller, why don't you just call the Action Method, like any other method in c#?
So instead of
return RedirectToAction("select-appointment", request);
You write
return SelectAppoinment(request, model);
I know, not exactly what you asked for, but at least it might be a workaround.

C# OData4 Apply OData query without controller returning Queryable

I currently have an C# WebAPI that uses a version of OData that we wrote. We want to start using Microsoft's OData4 which can do more then our custom implementation.
Creating a controller that extends the ODataController I can create a controller that automatically queries based on the query string. (Shown below)
The problem is that it returns the results of the query when I want it to return the Result object which includes additional data. When I set the return type to Result though it will no longer apply the query string.
How can I use the automatic queryable implementation and still return my own object? I've tried making a public method that returns the correct object and calls a private method returning the queryable but it doesn't filter the queryable correctly.
Am I on the right track, or are there other options?
public class CallController : ODataController
{
[EnableQuery]
public IQueryable<Call> GetCall()
{
var list = new List<Call>
{
new Call
{
Id = 4
},
new Call
{
Id = 9
},
new Call
{
Id = 1
}
};
return list.AsQueryable();
}
}
public class Call
{
public int Id { get; set; }
}
public class Result
{
public Call[] Calls { get; set; }
public string NewToken { get; set; }
public string Warning { get; set; }
}
Use ODataQuertOptions instead of [EnableQuery] attribute. Check https://learn.microsoft.com/en-us/aspnet/web-api/overview/odata-support-in-aspnet-web-api/supporting-odata-query-options#invoking-query-options-directly
You would need to intercept the response with an action filter attribute in the onactionexecuted and convert the value to whatever you want. It wouldn't be pretty since it wouldn't be clear what the method was truly returning. But I don't see any other option with odata as the result must be iquerable.
Under the hood, the EnableQuery attribute is executing the action it decorates to get an IQueryable<T>, converting the odata query into something that can be applied to the IQueryable<T>, applying it and returning the results.
In order to work, it needs an IQueryable<T>.
The ODataQueryOptions and example in Ihar's answer may give you what you want, but for various reasons it wasn't as useful to me as EnableQuery and so I ended up with an output formatter.
You can inspect the first output formatters in services.AddMvc(options => { options.OutputFormatters } in Startup.ConfigureServices and see that the first one has a bunch of different payload kinds.
I have been able to insert a custom error handler to handle ODataPayloadKind.Error payloads - re-writing the content returned from the server to remove stack traces etc if not in dev mode. I haven't looked into non-error cases, but you may be able to use what I have here as a starting point.

Web API with complex array parameters

Need help on this one. I have a WebAPI who can receive multiple ids as parameters. The user can call the API using 2 route:
First route:
api/{controller}/{action}/{ids}
ex: http://localhost/api/{controller}/{action}/id1,id2,[...],idN
Method signature
public HttpResponseMessage MyFunction(
string action,
IList<string> values)
Second route:
"api/{controller}/{values}"
ex: http://localhost/api/{controller}/id1;type1,id2;type2,[...],idN;typeN
public HttpResponseMessage MyFunction(
IList<KeyValuePair<string, string>> ids)
Now I need to pass a new parameter to the 2 existing route. The problem is this parameter is optional and tightly associated with the id value. I made some attempt like a method with KeyValuePair into KeyValuePair parameter but its results in some conflict between routes.
What I need is something like that :
ex: http://localhost/api/{controller}/{action}/id1;param1,id2;param2,[...],idN;paramN
http://localhost/api/{controller}/id1;type1;param1,id2;type2;param2,[...],idN;typeN;paramN
You might be able to deal with it by accepting an array:
public HttpResponseMessage MyFunction(
string action,
string[] values)
Mapping the route as:
api/{controller}/{action}
And using the query string to supply values:
GET http://server/api/Controller?values=1&values=2&values=3
Assumption: You are actually doing some command with the data.
If your payload to the server is getting more complex than a simple route can handle, consider using a POST http verb and send it to the server as JSON instead of mangling the uri to shoehorn it in as a GET.
Different assumption: You are doing a complex fetch and GET is idiomatically correct for a RESTFUL service.
Use a querystring, per the answer posted by #TrevorPilley
Looks like a good scenario for a custom model binder. You can handle your incoming data and detect it your self and pass it to your own type to use in your controller. No need to fight with the built in types.
See here.
From the page (to keep the answer on SO):
Model Binders
A more flexible option than a type converter is to create a custom
model binder. With a model binder, you have access to things like the
HTTP request, the action description, and the raw values from the
route data.
To create a model binder, implement the IModelBinder interface. This
interface defines a single method, BindModel:
bool BindModel(HttpActionContext actionContext, ModelBindingContext
bindingContext);
Here is a model binder for GeoPoint objects.
public class GeoPointModelBinder : IModelBinder {
// List of known locations.
private static ConcurrentDictionary<string, GeoPoint> _locations
= new ConcurrentDictionary<string, GeoPoint>(StringComparer.OrdinalIgnoreCase);
static GeoPointModelBinder()
{
_locations["redmond"] = new GeoPoint() { Latitude = 47.67856, Longitude = -122.131 };
_locations["paris"] = new GeoPoint() { Latitude = 48.856930, Longitude = 2.3412 };
_locations["tokyo"] = new GeoPoint() { Latitude = 35.683208, Longitude = 139.80894 };
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(GeoPoint))
{
return false;
}
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
bindingContext.ModelName);
if (val == null)
{
return false;
}
string key = val.RawValue as string;
if (key == null)
{
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "Wrong value type");
return false;
}
GeoPoint result;
if (_locations.TryGetValue(key, out result) || GeoPoint.TryParse(key, out result))
{
bindingContext.Model = result;
return true;
}
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "Cannot convert value to Location");
return false;
} } A model binder gets raw input values from a value provider. This design separates two distinct functions:
The value provider takes the HTTP request and populates a dictionary
of key-value pairs. The model binder uses this dictionary to populate
the model. The default value provider in Web API gets values from the
route data and the query string. For example, if the URI is
http://localhost/api/values/1?location=48,-122, the value provider
creates the following key-value pairs:
id = "1" location = "48,122" (I'm assuming the default route template,
which is "api/{controller}/{id}".)
The name of the parameter to bind is stored in the
ModelBindingContext.ModelName property. The model binder looks for a
key with this value in the dictionary. If the value exists and can be
converted into a GeoPoint, the model binder assigns the bound value to
the ModelBindingContext.Model property.
Notice that the model binder is not limited to a simple type
conversion. In this example, the model binder first looks in a table
of known locations, and if that fails, it uses type conversion.
Setting the Model Binder
There are several ways to set a model binder. First, you can add a
[ModelBinder] attribute to the parameter.
public HttpResponseMessage
Get([ModelBinder(typeof(GeoPointModelBinder))] GeoPoint location)
You
can also add a [ModelBinder] attribute to the type. Web API will use
the specified model binder for all parameters of that type.
[ModelBinder(typeof(GeoPointModelBinder))] public class GeoPoint {
// .... }
I found a solution.
First, I created a class to override the
KeyValuePair<string, string>
type to add a third element (I know it's not really a pair!). I could have use Tuple type also:
public sealed class KeyValuePair<TKey, TValue1, TValue2>
: IEquatable<KeyValuePair<TKey, TValue1, TValue2>>
To use this type with parameter, I create an
ActionFilterAttribute
to split (";") the value from the url and create a KeyValuePair (third element is optional)
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ActionArguments.ContainsKey(ParameterName))
{
var keyValuePairs = /* function to split parameters */;
actionContext.ActionArguments[ParameterName] =
keyValuePairs.Select(
x => x.Split(new[] { "," }, StringSplitOptions.None))
.Select(x => new KeyValuePair<string, string, string>(x[0], x[1], x.Length == 3 ? x[2] : string.Empty))
.ToList();
}
}
And finally, I add the action attribute filter to the controller route and change the parameter type:
"api/{controller}/{values}"
ex: http://localhost/api/{controller}/id1;type1;param1,id2;type2,[...],idN;typeN;param3
[MyCustomFilter("ids")]
public HttpResponseMessage MyFunction(
IList<KeyValuePair<string, string, string>> ids)
I could use some url parsing technique, but the ActionFilterAttribute is great and the code is not a mess finally!

Asp.Net MVC 2 - Bind a model's property to a different named value

Update (21st Sept 2016) - Thanks to Digbyswift for commenting that this solution still works in MVC5 also.
Update (30th April 2012) - Note to people stumbling across this question from searches etc - the accepted answer is not how I ended up doing this - but I left it accepted because it might have worked in some cases. My own answer contains the final solution I used, which is reusable and will apply to any project.
It's also confirmed to work in v3 and v4 of the MVC framework.
I have the following model type (the names of the class and its properties have been changed to protect their identities):
public class MyExampleModel
{
public string[] LongPropertyName { get; set; }
}
This property is then bound to a bunch (>150) of check boxes, where each one's input name is of course LongPropertyName.
The form submits to url with an HTTP GET, and say the user selects three of those checkboxes - the url will have the query string ?LongPropertyName=a&LongPropertyName=b&LongPropertyName=c
Big problem then is that if I select all (or even just over half!) the checkboxes, I exceed the maximum query string length enforced by the request filter on IIS!
I do not want to extend that - so I want a way to trim down this query string (I know I can just switch to a POST - but even so I still want to minimize the amount of fluff in the data sent by the client).
What I want to do is have the LongPropertyName bound to simply 'L' so the query string would become ?L=a&L=b&L=c but without changing the property name in code.
The type in question already has a custom model binder (deriving from DefaultModelBinder), but it's attached to its base class - so I don't want to put code in there for a derived class. All the property binding is currently performed by the standard DefaultModelBinder logic, which I know uses TypeDescriptors and Property Descriptors etc from System.ComponentModel.
I was kinda hoping that there might be an attribute I could apply to the property to make this work - is there? Or should I be looking at implementing ICustomTypeDescriptor?
In response to michaelalm's answer and request - here's what I've ended up doing. I've left the original answer ticked mainly out of courtesy since one of the solutions suggested by Nathan would have worked.
The output of this is a replacement for DefaultModelBinder class which you can either register globally (thereby allowing all model types to take advantage of aliasing) or selectively inherit for custom model binders.
It all starts, predictably with:
/// <summary>
/// Allows you to create aliases that can be used for model properties at
/// model binding time (i.e. when data comes in from a request).
///
/// The type needs to be using the DefaultModelBinderEx model binder in
/// order for this to work.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class BindAliasAttribute : Attribute
{
public BindAliasAttribute(string alias)
{
//ommitted: parameter checking
Alias = alias;
}
public string Alias { get; private set; }
}
And then we get this class:
internal sealed class AliasedPropertyDescriptor : PropertyDescriptor
{
public PropertyDescriptor Inner { get; private set; }
public AliasedPropertyDescriptor(string alias, PropertyDescriptor inner)
: base(alias, null)
{
Inner = inner;
}
public override bool CanResetValue(object component)
{
return Inner.CanResetValue(component);
}
public override Type ComponentType
{
get { return Inner.ComponentType; }
}
public override object GetValue(object component)
{
return Inner.GetValue(component);
}
public override bool IsReadOnly
{
get { return Inner.IsReadOnly; }
}
public override Type PropertyType
{
get { return Inner.PropertyType; }
}
public override void ResetValue(object component)
{
Inner.ResetValue(component);
}
public override void SetValue(object component, object value)
{
Inner.SetValue(component, value);
}
public override bool ShouldSerializeValue(object component)
{
return Inner.ShouldSerializeValue(component);
}
}
This proxies a 'proper' PropertyDescriptor that is normally found by the DefaultModelBinder but presents its name as the alias.
Next we have the new model binder class:
UPDATED WITH #jsabrooke's suggestion below
public class DefaultModelBinderEx : DefaultModelBinder
{
protected override System.ComponentModel.PropertyDescriptorCollection
GetModelProperties(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
var toReturn = base.GetModelProperties(controllerContext, bindingContext);
List<PropertyDescriptor> additional = new List<PropertyDescriptor>();
//now look for any aliasable properties in here
foreach (var p in
this.GetTypeDescriptor(controllerContext, bindingContext)
.GetProperties().Cast<PropertyDescriptor>())
{
foreach (var attr in p.Attributes.OfType<BindAliasAttribute>())
{
additional.Add(new AliasedPropertyDescriptor(attr.Alias, p));
if (bindingContext.PropertyMetadata.ContainsKey(p.Name)
&& !string.Equals(p.Name, attr.Alias, StringComparison.OrdinalIgnoreCase)))
{
bindingContext.PropertyMetadata.Add(
attr.Alias,
bindingContext.PropertyMetadata[p.Name]);
}
}
}
return new PropertyDescriptorCollection
(toReturn.Cast<PropertyDescriptor>().Concat(additional).ToArray());
}
}
And, then technically, that's all there is to it. You can now register this DefaultModelBinderEx class as the default using the solution posted as the answer in this SO: Change the default model binder in asp.net MVC, or you can use it as a base for your own model binder.
Once you've selected your pattern for how you want the binder to kick in, you simply apply it to a model type as follows:
public class TestModelType
{
[BindAlias("LPN")]
//and you can add multiple aliases
[BindAlias("L")]
//.. ad infinitum
public string LongPropertyName { get; set; }
}
The reason I chose this code was because I wanted something that would work with custom type descriptors as well as being able to work with any type. Equally, I wanted the value provider system to be used still in sourcing the model property values. So I've changed the meta data that the DefaultModelBinder sees when it starts binding. It's a slightly more long-winded approach - but conceptually it's doing at the meta data level exactly what you want it to do.
One potentially interesting, and slightly annoying, side effect will be if the ValueProvider contains values for more than one alias, or an alias and the property by it's name. In this case, only one of the retrieved values will be used. Difficult to think of a way of merging them all in a type-safe way when you're just working with objects though. This is similar, though, to supplying a value in both a form post and query string - and I'm not sure exactly what MVC does in that scenario - but I don't think it's recommended practise.
Another problem is, of course, that you must not create an alias that equals another alias, or indeed the name of an actual property.
I like to apply my model binders, in general, using the CustomModelBinderAttribute class. The only problem with this can be if you need to derive from the model type and change it's binding behaviour - since the CustomModelBinderAttribute is inherited in the attribute search performed by MVC.
In my case this is okay, I'm developing a new site framework and am able to push new extensibility into my base binders using other mechanisms to satisfy these new types; but that won't be the case for everybody.
You can use the BindAttribute to accomplish this.
public ActionResult Submit([Bind(Prefix = "L")] string[] longPropertyName) {
}
Update
Since the 'longPropertyName' parameter is part of the model object, and not an independent parameter of the controller action, you have a couple of other choices.
You could keep the model and the property as independent parameters to your action and then manually merge the data together in the action method.
public ActionResult Submit(MyModel myModel, [Bind(Prefix = "L")] string[] longPropertyName) {
if(myModel != null) {
myModel.LongPropertyName = longPropertyName;
}
}
Another option would be implementing a custom Model Binder that performs the parameter value assignment (as above) manually, but that is most likely overkill. Here's an example of one, if you're interested: Flags Enumeration Model Binder.
would this be a solution similar to yours Andras? i hope you could post your answer as well.
controller method
public class MyPropertyBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
for (int i = 0; i < propertyDescriptor.Attributes.Count; i++)
{
if (propertyDescriptor.Attributes[i].GetType() == typeof(BindingNameAttribute))
{
// set property value.
propertyDescriptor.SetValue(bindingContext.Model, controllerContext.HttpContext.Request.Form[(propertyDescriptor.Attributes[i] as BindingNameAttribute).Name]);
break;
}
}
}
}
Attribute
public class BindingNameAttribute : Attribute
{
public string Name { get; set; }
public BindingNameAttribute()
{
}
}
ViewModel
public class EmployeeViewModel
{
[BindingName(Name = "txtName")]
public string TestProperty
{
get;
set;
}
}
then to use the Binder in the controller
[HttpPost]
public ActionResult SaveEmployee(int Id, [ModelBinder(typeof(MyPropertyBinder))] EmployeeViewModel viewModel)
{
// do stuff here
}
the txtName form value should be set to the TestProperty.
This should probably be a shorter comment on Andras Zoltan's answer but don't have enough reputation, sorry.
Thanks for the solution, I've just used it and it still works great! However, some of my properties have an alias with the same name, but different case e.g.
[BindAlias("signature")]
public string Signature { get; set; }
These throw an error when the custom model binder tries to add the aliases to the
PropertyMetadata dictionary, as their main property name versions have already been added by the base model binder, and the model binding is case-insensitive.
To solve this, just do a case insensitive check -
replace
if (bindingContext.PropertyMetadata.ContainsKey(p.Name))
with
if (bindingContext.PropertyMetadata.ContainsKey(p.Name)
&& !string.Equals(p.Name, attr.Alias, StringComparison.OrdinalIgnoreCase))
So I've spent most of the day trying to figure out why I couldn't get this to work. Since I'm making my calls from a System.Web.Http.ApiController turns out that you can't use the DefaultPropertyBinder solution as mentioned above but instead must us an IModelBinder class.
the class that I've wound up writing to replace #AndreasZoltan's foundational work as written above is as follows:
using System.Reflection;
using System.Web;
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;
using QueryStringAlias.Attributes;
namespace QueryStringAlias.ModelBinders
{
public class AliasModelBinder : IModelBinder
{
private bool TryAdd(PropertyInfo pi, NameValueCollection nvc, string key, ref object model)
{
if (nvc[key] != null)
{
try
{
pi.SetValue(model, Convert.ChangeType(nvc[key], pi.PropertyType));
return true;
}
catch (Exception e)
{
Debug.WriteLine($"Skipped: {pi.Name}\nReason: {e.Message}");
}
}
return false;
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
Type bt = bindingContext.ModelType;
object model = Activator.CreateInstance(bt);
string QueryBody = actionContext.Request.Content.ReadAsStringAsync().Result;
NameValueCollection nvc = HttpUtility.ParseQueryString(QueryBody);
foreach (PropertyInfo pi in bt.GetProperties())
{
if (TryAdd(pi, nvc, pi.Name, ref model))
{
continue;
};
foreach (BindAliasAttribute cad in pi.GetCustomAttributes<BindAliasAttribute>())
{
if (TryAdd(pi, nvc, cad.Alias, ref model))
{
break;
}
}
}
bindingContext.Model = model;
return true;
}
}
}
In order to ensure that this runs as part of a WebAPI call you must also add config.BindParameter(typeof(TestModelType), new AliasModelBinder()); in the Regiser portion of your WebApiConfig.
If you are using this method, you also must remove [FromBody] from your method signature.
[HttpPost]
[Route("mytestendpoint")]
[System.Web.Mvc.ValidateAntiForgeryToken]
public async Task<MyApiCallResult> Signup(TestModelType tmt) // note that [FromBody] does not appear in the signature
{
// code happens here
}
Note that this work builds on the answer above, using the QueryStringAlias samples.
At the moment this would likely fail in the case where TestModelType had complex nested types. Ideally there are a few other things:
handle complex nested types robustly
enable an attribute on the class to activate the IModelBuilder as opposed to in the registration
enable the same IModelBuilder to work in both Controllers and ApiControllers
But for now I'm satisfied with this for my own needs. Hopefully someone finds this piece useful.

Query String to Object with strongly typed properties

Let’s say we track 20 query string parameters in our site. Each request which comes will have only a subset of those 20 parameters. But we definitely look for all/most of the parameters which comes in each request.
We do not want to loop through the collection each time we are looking for a particular parameter initially or somewhere down the pipeline in the code. So we loop once through the query string collection, convert string values to their respective types (enums, int, string etc.), populate to QueryString object which is added to the context.
After that wherever its needed we will have a strongly typed properties in the QueryString object which is easy to use and we maintain a standard.
public class QueryString
{
public int Key1{ get; private set; }
public SomeType Key2{ get; private set; }
private QueryString() { }
public static QueryString GetQueryString()
{
QueryString l_QS = new QueryString();
foreach (string l_Key in HttpContext.Current.Request.QueryString.AllKeys)
{
switch (l_Key)
{
case "key1":
l_QS.Key1= DoSomething(l_Key, HttpContext.Current.Request.QueryString[l_Key]);
break;
case "key2":
l_QS.Key2 = DoAnotherThing(l_Key, HttpContext.Current.Request.QueryString[l_Key]);
break;
}
}
return l_QS;
}
}
Any other solution to achieve this?
I am not sure if you are using it or not, but ASP.NET MVC will handle this for you. You can define all of your parameters in the action's signature, and any you don' t receive will be null.
Another solution would perhaps be a custom enum class(with one value for each parameter) with custom ToString() method. You could then reverse lookup the string to get the enum value, use that in your switch case. In case of invalid parameters, an exception can be thrown during reverse lookup.

Categories

Resources