ASP.NET Core 5.0 - POST on custom model - c#

I did it according to youtube tutorial, but unfortunately for me it behaves extremely oddly.
Simple scenario: add comment to a post.
public class CommentViewModel
{
public Post Post { get; set; }
public Comment Comment { get; set; }
}
<p>#Model.Post.Title</p>
<p>#Model.Post.Body</p>
<form method="post" asp-action="NewComment">
<input asp-for="Post.Id" hidden />
<div class="border p-3">
#*<div asp-validation-summary="ModelOnly" class="text-danger"></div>*#
<div class="form-group row">
<h2 class="text-info pl-3">Write new comment</h2>
</div>
<div class="form-group row">
<label asp-for="Comment.Body"></label>
<textarea asp-for="Comment.Body" class="form-control"></textarea>
<span asp-validation-for="Comment.Body" class="text-danger"></span>
</div>
</div>
<div class="form-group row">
<div class="col-8 offset-2 row">
<div class="col">
<input type="submit" class="btn btn-info w-100" value="Create" />
</div>
<div class="col">
<a asp-action="Index" class="btn btn-success w-100"><i class="fas fa-sign-out-alt"></i> Back</a>
</div>
</div>
</div>
</form>
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult NewComment(CommentViewModel model)
{
if (ModelState.IsValid)
{
_service.AddNewComment(new Guid(), model.Post.Id, model.Comment.Body);
return RedirectToAction("Index");
}
return View();
}
Models:
public class Post
{
[BsonElement("id")]
public Guid Id { get; set; }
[BsonElement("title")]
[Required]
[MaxLength(64)]
public string Title { get; set; }
[BsonElement("post_body")]
[Required]
[Display(Name = "Post")]
[MaxLength(256)]
public string Body { get; set; }
}
public class Comment
{
[BsonElement("id")]
public Guid Id { get; set; }
[BsonElement("post_id")]
public Guid PostId { get; set; }
[BsonElement("comment_body")]
[Required]
[Display(Name = "Comment")]
[MaxLength(128)]
public string Body { get; set; }
}
When fields are not filled red notification appears, as it should. But once fields are filled and user clicks on create, ModelState.IsValid is still false, and for some reason application tries to reload view, but returns exception on <p>#Model.Post.Title</p> NullReferenceException.
It's extremely weird behavior. Adding new post is almost identical except <input asp-for="Post.Id" hidden /> (since there are no relations to anything else), and it works flawlessly. Here things are glitching out.
Removing if (ModelState.IsValid) and return View(); absolutely fixes the issue. Both validation and POST works. But it should work even with it.
Any clues? All laws of logic say it should work. Otherwise I will be forced to keep it the weird way.

I don't see any weird here. It works according to your code. But you can fix a bug:
if (ModelState.IsValid)
{
_service.AddNewComment(new Guid(), model.Post.Id, model.Comment.Body);
return RedirectToAction("Index");
}
return View(model);
in this case you will not have a null reference exeption.
and by the way I am using this code to find what is invalid in a ModelState:
public static string ValidModelState(ModelStateDictionary modelState)
{
var errorMessage = "";
if (!modelState.IsValid)
{
foreach (var item in modelState.Values)
{
foreach (var modelError in item.Errors)
{
errorMessage += "\n" + "Error: " + modelError.ErrorMessage;
}
}
}
return errorMessage;
}
And using this instead of if (ModelState.IsValid):
var errorMessage = ValidModelState(ModelState);
if( !string.IsNullOrEmpty(errorMessage)).... errorMessage;

It says The Post field is required., except that property does not exist.
You have a [Display(Name = "Post")] on the Post.Body property, And I think the model in the view doesn't have value for Post Body, so the validation failed, and the error message for it is The Post field is required. Go and check it.

Related

ASP.NET MVC HttpPost async task not functioning correctly

I am trying to create a form in my actors page. Upon clicking the submit button, fields are validated and it should in theory submit, but it is not. I have tried renaming, creating a new function that intellisense suggests and my only ways of making this form to submit is either manually making the function go to _service.Add(actor); or by not going with the validation, but then if one of the required fields is not met, it throws an error in a different page, which is not ideal. I have no clue how to make this work, because the course, that I am recreating it from is able to do it just fine.
My code - controller:
namespace Cinema_World.Controllers
{
public class ActorsController : Controller
{
private readonly IActorsService _service;
public ActorsController(IActorsService service)
{
_service = service;
}
public async Task<IActionResult> Index()
{
var allActors = await _service.GetAll();
return View(allActors);
}
public async Task<IActionResult> Create()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Create([Bind("FirstName,MiddleName,LastName,BirthYear,BirthPlace")] ActorModel actor)
{
if (!ModelState.IsValid) //when i use break-points, this part gets stepped into
{// also this part
return View(actor); //this part too
} // and this is the final part, then it skips to the end and nothing happens in the browser
_service.Add(actor);
return RedirectToAction(nameof(Index));
}
}
}
My models:
namespace Cinema_World.Models
{
public class ActorModel
{
[Key]
public int ActorID { get; set; }
[Display(Name = "First name")]
[Required(ErrorMessage = "First name is a required field")]
[StringLength(100, MinimumLength = 1, ErrorMessage = "First name can be between 1 and 100 characters long!")]
public string FirstName { get; set; }
[Display(Name = "Middle name")]
[StringLength(100, MinimumLength = 1, ErrorMessage = "Middle name can be between 1 and 100 characters long!")]
public string? MiddleName { get; set; }
[Display(Name = "Last name")]
[Required(ErrorMessage = "Last name is a required field")]
[StringLength(100, MinimumLength = 1, ErrorMessage = "Last name can be between 1 and 100 characters long!")]
public string LastName { get; set; }
[Display(Name = "Year of Birth")]
[Required(ErrorMessage = "Year of birth is a required field")]
[Range(999,9999, ErrorMessage = "Input a year between 999 and 9999")]
public int BirthYear { get; set; }
[Display(Name = "Place of Birth")]
[Required(ErrorMessage = "Place of birth is a required field")]
[StringLength(100, MinimumLength = 1, ErrorMessage = "Name of the place can be between 1 and 100 characters long!")]
public string BirthPlace { get; set; }
public List<Actor_CinematographyModel> Actors_Cinematography { get; set; }
}
}
Code from my service that gets called, when form submit is successful.
namespace Cinema_World.Data.Services
{
public class ActorsService : IActorsService
{
private readonly ApplicationDbContext _context;
public ActorsService(ApplicationDbContext context)
{
_context = context;
}
public void Add(ActorModel Actor)
{
_context.Actors.Add(Actor);
_context.SaveChanges();
}
public void Delete(int ActorID)
{
throw new NotImplementedException();
}
public async Task<IEnumerable<ActorModel>> GetAll()
{
var result = await _context.Actors.ToListAsync();
return result;
}
public ActorModel GetById(int ActorID)
{
throw new NotImplementedException();
}
public ActorModel Update(int ActorID, ActorModel newActor)
{
throw new NotImplementedException();
}
}
}
Interface for this specific service:
namespace Cinema_World.Data.Services
{
public interface IActorsService
{
Task<IEnumerable<ActorModel>> GetAll();
ActorModel GetById(int ActorID);
void Add(ActorModel Actor);
ActorModel Update(int ActorID, ActorModel newActor);
void Delete(int ActorID);
}
}
View markup:
<div class="row text">
<div class="col-md-8 offset-2">
<p>
<h1>Add a new Actor!</h1>
</p>
<div class="row">
<div class="col-md-8 offset-2">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="FirstName" class="control-label"></label>
<input asp-for="FirstName" class="form-control" />
<span asp-validation-for="FirstName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="MiddleName" class="control-label"></label>
<input asp-for="MiddleName" class="form-control" />
<span asp-validation-for="MiddleName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="LastName" class="control-label"></label>
<input asp-for="LastName" class="form-control" />
<span asp-validation-for="LastName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="BirthYear" class="control-label"></label>
<input asp-for="BirthYear" class="form-control" />
<span asp-validation-for="BirthYear" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="BirthPlace" class="control-label"></label>
<input asp-for="BirthPlace" class="form-control" />
<span asp-validation-for="BirthPlace" class="text-danger"></span>
</div>
<div class="form-group mt-2">
<input type="submit" value="Create" class="btn btn-outline-success float-end"/>
<a class="btn btn-outline-dark" asp-action="Index">Show all</a>
</div>
</form>
</div>
</div>
</div>
</div>
If anything else is required, please, let me know. I have been stuck hard on this for a while already, and if I am not able to fix this, I cannot progress with my other forms.
Like I said before, I tried replacing the !ModelState.IsValid with ModelState.IsValid and putting the executing code in there, intellisense suggestions and even manually, using break-points and I did manage to get it to work like that, but is not an ideal choice.
My knowledge in ASP.NET MVC is basic, so perhaps I messed up something or missed something.
Just for clarification - the called service works, i am able to post data, but if validation is present in the same method, i am unable to post anything and the button does not do anything.
Turns out, the problem was not in the method, but the model.
public List<Actor_CinematographyModel> Actors_Cinematography { get; set; } was somehow conflicting and not allowing me to post data. after making it possible to be a null value, everything works. not sure, why it did it, maybe i set up my database tables wrong. this thread is no longer in question :)

How to validate several forms in one view using single view model in ASP.NET Core MVC?

I have a view with single view model called DataViewModel.
In this view I have more than one form. Now I can't set all properties to required because if I do so then if I click for example Save button which saves certain fields. There are some other fields that are not required for the save button but required for add button. How can I handle validation in this case?
Also, If I have an error in one action which is one form and I used return View(); it will show all errors of required fields that I actually don't need for this specific action. And If I used RedirectToAction(), then the page is returned with no error even if there were one.
DataViewModel
public class DataViewModel
{
[Required]
public string personName { get; set; }
[Required]
public string classChosen { get; set; }
[Required]
public string className { get; set; }
[Required]
public string ClassCode { get; set; }
[Required]
public string newPersonName { get; set; }
}
Index view
#model DataViewModel
<form asp-controller="Home" asp-action="AddPerson" method="post">
<input required="required" type="text" class="form-control scan" placeholder="New Person"
asp-for="newPersonName " />
<span asp-validation-for="newPersonName " class="text-danger"></span>
<input type="submit" class="model-close button btn btn-primary primary-btn"
style="width:auto;" value="Add" />
</form>
<form asp-controller="Home" asp-action="Index" method="post">
\\Here I'm adding person with class data
</form>
Home controller
[HttpPost]
public IActionResult AddPerson(DataViewModel model)
{
Person person = new Person();
if (model.newPersonName != null)
{
person.Name = model.newPersonName;
person.status = true;
var personName = dbContext.Person
.Where(w => w.Name == model.newPersonName)
.Select(w => w.Name)
.FirstOrDefault();
if (personName == null)
{
personRepository.AddPerson(person);
}
else
{
ModelState.AddModelError("newPersonName", "Name already exists");
}
}
ModelState.AddModelError("newPersonName", "Please enter valid value");
return View("Index");
}
[HttpPost]
public IActionResult Index(DataViewModel model)
{
// Code for adding classes for person
return View(model);
}
How can I handle such a case?
Because when I click add for adding new person with return view it shows all errors even ones that are not related to adding new person. and same for same. How can I separate the validation for several forms in one view using single view model
I think you can try to only use client side validation,If the data is not required,the form data will not be passed to the action:
public class DataViewModel
{
public string personName { get; set; }
public string classChosen { get; set; }
public string className { get; set; }
public string ClassCode { get; set; }
public string newPersonName { get; set; }
}
Index view:
<form asp-controller="Home" asp-action="AddPerson" method="post">
<input required="required" type="text" class="form-control scan" placeholder="New Person"
asp-for="newPersonName " />
<span asp-validation-for="newPersonName " class="text-danger"></span>
<input type="submit" class="model-close button btn btn-primary primary-btn"
style="width:auto;" value="Add" />
</form>
<form asp-controller="Home" asp-action="Index" method="post">
input required="required" type="text" class="form-control scan" placeholder="New Person"
asp-for="className " />
<span asp-validation-for="className " class="text-danger"></span>
<input type="submit" class="model-close button btn btn-primary primary-btn"
style="width:auto;" value="Save" />
</form>

ASP.NET Core MVC model validation in viewmodel with base model

I am developing a web app with ASP.NET Core MVC, and I have a problem with model validation.
When I set validation in class and then use it in view model, validation does not work. How can I struggle with it?
This is my code:
public class Il : IEntity
{
[Required(ErrorMessage = "Kodu boş geçilemez")]
public int IlKodu { get; set; }
public int UlkeId { get; set; }
[Required(ErrorMessage = "Ad boş geçilemez")]
public string Ad { get; set; }
public int Id { get; set; }
public DateTime? KayitTarihi { get; set; }
public DateTime? GuncellemeTarihi { get; set; }
}
ViewModel class;
public class IlAddViewModel
{
public Il Il { get; set; }
public List<Ulke> Ulkeler{ get; set; }
}
Then the view:
<div class="container pt-4 eklemeDiv col-4">
<form asp-controller="Il" asp-action="Add" asp-area="Admin" method="post">
<div class="form-group">
<label asp-for="Il.IlKodu">Il Kodu</label>
<input asp-for="Il.IlKodu" class="form-control" placeholder="İl Kodu Giriniz">
<span asp-validation-for="Il.IlKodu" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Il.Ad">Il Kodu</label>
<input asp-for="Il.Ad" class="form-control" placeholder="İl Kodu Giriniz">
<span asp-validation-for="Il.Ad" class="text-danger"></span>
</div>
<div class="form-group">
<label >Ülke</label>
<select style="width: 100%;height:30px" id="selectIl" asp-for="Il.UlkeId"
asp-items="#(new SelectList(Model.Ulkeler,"Id","Ad"))">
<option>Lütfen Seçim Yapınız</option>
</select>
</div>
<input id="btnIlEkle" type="submit" value="Ekle" class="btn btn-xs btn-success" />
<a class="btn btn-xs btn-primary" asp-action="Anasayfa" asp-area="Admin" asp-controller="Admin"><i class="fas fa-chevron-left"></i>Anasayfaya Dön</a>
</form>
</div>
The view model does not show validation messages in view. And also how can I show validation messages for list elements?
My friends, all parts of my code are correct for structure, just one error is missing and finally i found it today. I am sharing this solution to help others;
Also controller code;
[HttpPost]
public async Task<ActionResult> Add(IlAddViewModel ilAddViewModel)
{
var kayitVarmi = _ilService.BenzerKayitVarMi(ilAddViewModel.Il.IlKodu, ilAddViewModel.Il.Ad);
if (kayitVarmi)
{
TempData.Add("Hata", "Böyle bir kayıt mevcut");
return RedirectToAction("Add");
}
else
{
if (ModelState.IsValid && !kayitVarmi)
{
ilAddViewModel.Il.KayitTarihi = DateTime.Now;
await _ilService.Add(ilAddViewModel.Il);
TempData.Add("Message", String.Format("{0} başarıyla eklendi", ilAddViewModel.Il.Ad));
return RedirectToAction("Anasayfa", "Admin", new { Area = "Admin" });
}
}
return RedirectToAction("Add");
}
To achieve this validation error, you must add necessary javascript packages as below;
<script src="~.../jquery-validation/dist/jquery.validate.js"></script>
<script src="~.../jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.js"></script>
These javascript packages help to show validation messages without making post to action method.
In the controller you will have access to check the model state to verify if it is valid or not. The controller code should like this
public class HomeController : Controller
{
// GET: Home
public ActionResult Index()
{
return View();
}
[HttpGet]
public ActionResult Create()
{
return View();
}
[HttpPost]
public ActionResult Create(Employee employee)
{
try
{
//Here ModelState.IsValid will be true in case all the required fields that are mentioned in employee class
if (ModelState.IsValid)
{
return RedirectToAction("Index");
}
return View();
}
catch (Exception)
{
return View();
}
}
}

Model isn't validating

So, when putting in text for my model it is always valid, even though I explicitly asked for it to have a minLength despite it being empty or being less than the minLength.
Models:
public class CommentaarCreate_VM
{
public Stad Stad { get; set; }
[Required]
public Commentaar Commentaar { get; set; }
}
public class Commentaar
{
[Key]
public int CommentaarId { get; set; }
[Required]
public string UserId { get; set; }
[Required]
public int StadId { get; set; }
[Required(AllowEmptyStrings=false, ErrorMessage="You need to enter a comment of valid length")]
[MinLength(5, ErrorMessage ="You need to enter a comment of valid length")]
public string CommentaarText { get; set; }
[Required]
[DataType(DataType.DateTime)]
public DateTime Tijdstip { get; set; }
}
View:
#model DataGent.Web.ViewModels.CommentaarCreate_VM
#{
ViewData["Title"] = "Create new comment";
}
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="All" class="text-danger"></div>
<input type="hidden" asp-for="Stad.Id" />
<input type="hidden" asp-for="Stad.Naam" />
<input type="hidden" value="#Html.AntiForgeryToken()" />
<div class="form-group">
<label asp-for="Commentaar" class="control-label"></label>
<input asp-for="Commentaar" class="form-control" />
<span asp-validation-for="Commentaar.CommentaarText" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>
</div>
Controller action:
public ActionResult Create(int id)
{
CommentaarCreate_VM vm = new CommentaarCreate_VM()
{
Stad = _dataGentService.GetStadFromId(id),
Commentaar = null
};
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind("CommentaarText, Tijdstip")] int id, IFormCollection collection) //Bind = protect from overposting
{
try
{
// Creating object to POST
Commentaar commentaar = new Commentaar
{
UserId = _userManager.GetUserId(HttpContext.User),
StadId = id,
CommentaarText = collection["Commentaar"],
Tijdstip = DateTime.Now
};
var result = _dataGentService.PostCommentaar(commentaar);
return RedirectToAction(nameof(Index));
}
catch
{
return View();
}
}
Is there something I'm missing? I thought all the work, except for the dataannotations, was done by MVC?
Your input is:
<input asp-for="Commentaar" class="form-control" />
You have to change asp-for from Commentaar to Commentaar.CommentaarText so that it is validated:
<div class="form-group">
<label asp-for="Commentaar.CommentaarText" class="control-label"></label>
<input asp-for="Commentaar.CommentaarText" class="form-control" />
<span asp-validation-for="Commentaar.CommentaarText" class="text-danger"></span>
</div>
Update:
Initialize Commentaar object in your viewmodel before you pass it to the view:
public ActionResult Create(int id)
{
CommentaarCreate_VM vm = new CommentaarCreate_VM()
{
Stad = _dataGentService.GetStadFromId(id),
Commentaar = new Commentaar()
};
return View(vm);
}
A good practice is to use ModelState.IsValid on your post methods, in order to check properties of the model that is being sent. Said that, ModelState.IsValid checks for Data Annotations written by you on your Model.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind("CommentaarText, Tijdstip")] int id, IFormCollection collection) //Bind = protect from overposting
{
if(ModelState.IsValid)
{
//If it is valid, do all your business logic, like creating a new entry.
}
else
{
//Handle it
return View();
}
}
Another thing is that I see that you use ViewModels which is good. So you could just send your viewmodel as a parameter for your action. You could do that as follows:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(CommentaarCreate_VM viewmodel)
{
if(ModelState.IsValid)
{
//It is valid
//All your logic
}
else
{
//Not valid
return View(Viewmodel model)
}
}
By doing this, you have to add data annotations to CommentaarCreate_VM
public class CommentaarCreate_VM
{
public Stad Stad { get; set; }
[Required(AllowEmptyStrings=false, ErrorMessage="You need to enter a comment of valid length")]
[MinLength(5, ErrorMessage ="You need to enter a comment of valid length")]
public Commentaar Commentaar { get; set; }
}
So I've found atleast somewhat of a solution, but the underlying problem still stands.
The problem is that in the controller the Modelstate.IsValid is always true, even if some of the models should not be valid so it just does what I want it to before redirecting to another page.
Solution is that I can get the error messages working if in the controller I check if the string is null or empty, and if so just Return(viewmodel), and that gets the error messages working.
Obviously, the Modelstate.IsValid SHOULDNT be returning true, and I still don't know why it does.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind("CommentaarText, Tijdstip")] int id, CommentaarCreate_VM viewModel, IFormCollection collection) //Bind = protect from overposting
{
try
{
//If incoming string is null or empty
if (string.IsNullOrEmpty(collection["Commentaar"]))
{
return View(viewModel);
}
//This always returns true. It really shouldn't, because otherwise I wouldn't need that earlier check.
//If the model isn't valid in the View, this one should be false, right?
if (ModelState.IsValid)
{
// Creating object to POST
//.....
return RedirectToAction(nameof(Index));
}
return View();
}
catch
{
return View();
}
}

ViewModel data is all null when passed to my controller? [duplicate]

This question already has answers here:
Asp.Net MVC: Why is my view passing NULL models back to my controller?
(2 answers)
Closed 6 years ago.
I'm trying to follow best practices to add data validation to my UI. I want to add the data validation to the ViewModel and then if that data is valid, submit the form to the controller. However, the data the controller receives is always null values. Any idea what I'm doing wrong? This whole MVVC architecture is confusing me. I had it working when I was submitting the form using the model but I can't get data validation on the model?
Controller:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> CreateResource(AddResourceViewModel model)
{
if (ModelState.IsValid)
{
await (//does something);
}
return View();
}
ModelView:
public class AddResourceViewModel
{
public string User { get; set; }
public string ResourceName { get; set; }
public int ResourceId { get; set; }
public string Model { get; set; }
public float Latitude { get; set; }
public float Longitude { get; set; }
public decimal Amount { get; set; }
public int UnitId { get; set; }
public int PrimaryEsfId { get; set; }
public IEnumerable<string> Capabilities { get; set; }
public IEnumerable<int> AdditionalEsfs { get; set; }
public Resource Resource { get; set; }
}
Beginning of cshtml form:
#model erms.ViewModel.AddResourceViewModel
<form asp-controller="AddResource" asp-action="NewResource" method="post" class="form-horizontal">
<div class="panel panel-default">
<div class="panel-body">
<form asp-controller="AddResource" asp-action="CreateResource" method="post" class="form-horizontal">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="ResourceId" class="col-md-2 control-label">Resource ID:</label>
<div class="col-md-10">
<input asp-for="ResourceId" class="form-control" value="#Model.Resource.ResourceId" readonly />
<span asp-validation-for="ResourceId" class="text-danger"></span>
</div>
</div>
replace return View(); to return View(model);.
This kind of stuff makes me so frustrated when learning new architecture but I've figured it out. It is a naming convention issue. I've addressed the issue and it is now working properly:
The conflicting names were:
public string Model { get; set; }
from my ViewModel, and:
public async Task<IActionResult> NewResource(AddResourceViewModel model)
from my controller. So the Model is conflicting with the model...
According to: http://ideasof.andersaberg.com/development/aspnet-mvc-4-model-binding-null-on-post
Do not name your incoming variables in the Action the same as you do in the model being posted. That will mess up the Model Binder.
Perhaps I'm thinking the problem would be you're not using razor to submit the form. This is what I have in mind:
#model <AddResourceViewModel>
#using(Html.BeginForm()) {
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="ResourceId" class="col-md-2 control-label">Resource ID:</label>
<div class="col-md-10">
#Html.TextboxFor(x => x.ResourceId)
#Html.ValidationMessageFor(x => x.ResourceId)
</div>
</div>
}
Since this is what I usually use to validate my form, perhaps this is worth considering. I might be wrong though.

Categories

Resources