Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@gistlyn
Last active July 27, 2019 21:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gistlyn/33873ed2857b2c5a9623629c6210d665 to your computer and use it in GitHub Desktop.
Save gistlyn/33873ed2857b2c5a9623629c6210d665 to your computer and use it in GitHub Desktop.
Contacts Validation Example
using System;
using System.Linq;
using System.Collections.Generic;
using ServiceStack;
using ServiceStack.Script;
using ServiceStack.Validation;
using MyApp.ServiceModel;
using MyApp.ServiceModel.Types;
namespace MyApp
{
public class ConfigureContacts : IConfigureAppHost, IAfterInitAppHost
{
public void Configure(IAppHost appHost)
{
//Register required plugins if not registered already
appHost.Plugins.AddIfNotExists(new ValidationFeature());
appHost.Plugins.AddIfNotExists(new SharpPagesFeature());
appHost.AssertPlugin<SharpPagesFeature>().ScriptMethods.Add(new ContactScripts());
// Register Custom Auto Mapping for converting Contact Data Model to Contact DTO
AutoMapping.RegisterConverter((Data.Contact from) =>
from.ConvertTo<ServiceModel.Types.Contact>(skipConverters:true));
}
public void AfterInit(IAppHost appHost)
{
View.NavItems.Add(new NavItem {
Label = "Contacts",
Href = "/contacts/",
});
}
}
// Custom filters for App data sources and re-usable UI snippets in ServiceStack Sharp Pages
public class ContactScripts : ScriptMethods
{
static Dictionary<string, string> Colors = new Dictionary<string, string> {
{"#ffa4a2", "Red"},
{"#b2fab4", "Green"},
{"#9be7ff", "Blue"}
};
public Dictionary<string, string> contactColors() => Colors;
private static List<KeyValuePair<string, string>> Titles => EnumUtils.GetValues<Title>()
.Where(x => x != Title.Unspecified)
.ToKeyValuePairs();
public List<KeyValuePair<string, string>> contactTitles() => Titles;
private static List<string> FilmGenres => EnumUtils.GetValues<FilmGenres>().Map(x => x.ToDescription());
public List<string> contactGenres() => FilmGenres;
}
}
using System;
using System.Linq;
using System.Drawing;
using System.Threading;
using System.Globalization;
using System.Collections.Concurrent;
using ServiceStack;
using ServiceStack.Script;
using ServiceStack.FluentValidation;
using MyApp.ServiceModel;
using MyApp.ServiceModel.Types;
namespace MyApp.ServiceInterface
{
public class CreateContactValidator : AbstractValidator<CreateContact>
{
public CreateContactValidator()
{
RuleFor(r => r.Title).NotEqual(Title.Unspecified).WithMessage("Please choose a title");
RuleFor(r => r.Name).NotEmpty();
RuleFor(r => r.Color).Must(x => x.IsValidColor()).WithMessage("Must be a valid color");
RuleFor(r => r.FilmGenres).NotEmpty().WithMessage("Please select at least 1 genre");
RuleFor(r => r.Age).GreaterThan(13).WithMessage("Contacts must be older than 13");
RuleFor(x => x.Agree).Equal(true).WithMessage("You must agree before submitting");
}
}
[Authenticate] // Limit to Authenticated Users
[DefaultView("/contacts")] // Render custom HTML View for HTML Requests
public class ContactServices : Service
{
private static int Counter = 0;
internal static readonly ConcurrentDictionary<int, Data.Contact> Contacts = new ConcurrentDictionary<int, Data.Contact>();
public object Any(GetContacts request)
{
var userId = this.GetUserId();
return new GetContactsResponse
{
Results = Contacts.Values
.Where(x => x.UserAuthId == userId)
.OrderByDescending(x => x.Id)
.Map(x => x.ConvertTo<Contact>())
};
}
public object Any(GetContact request) =>
Contacts.TryGetValue(request.Id, out var contact) && contact.UserAuthId == this.GetUserId()
? (object)new GetContactResponse { Result = contact.ConvertTo<Contact>() }
: HttpError.NotFound($"Contact was not found");
public object Any(CreateContact request)
{
var newContact = request.ConvertTo<Data.Contact>();
newContact.Id = Interlocked.Increment(ref Counter);
newContact.UserAuthId = this.GetUserId();
newContact.CreatedDate = newContact.ModifiedDate = DateTime.UtcNow;
var contacts = Contacts.Values.ToList();
var alreadyExists = contacts.Any(x => x.UserAuthId == newContact.UserAuthId && x.Name == request.Name);
if (alreadyExists)
throw new ArgumentException($"You already have a contact named '{request.Name}'", nameof(request.Name));
Contacts[newContact.Id] = newContact;
return new CreateContactResponse { Result = newContact.ConvertTo<Contact>() };
}
public void Any(DeleteContact request)
{
if (Contacts.TryGetValue(request.Id, out var contact) && contact.UserAuthId == this.GetUserId())
Contacts.TryRemove(request.Id, out _);
}
}
public class UpdateContactValidator : AbstractValidator<UpdateContact>
{
public UpdateContactValidator()
{
RuleFor(r => r.Id).GreaterThan(0);
RuleFor(r => r.Title).NotEqual(Title.Unspecified).WithMessage("Please choose a title");
RuleFor(r => r.Name).NotEmpty();
RuleFor(r => r.Color).Must(x => x.IsValidColor()).WithMessage("Must be a valid color");
RuleFor(r => r.FilmGenres).NotEmpty().WithMessage("Please select at least 1 genre");
RuleFor(r => r.Age).GreaterThan(13).WithMessage("Contacts must be older than 13");
}
}
public class UpdateContactServices : Service
{
public object Any(UpdateContact request)
{
if (!ContactServices.Contacts.TryGetValue(request.Id, out var contact) || contact.UserAuthId != this.GetUserId())
throw HttpError.NotFound("Contact was not found");
contact.PopulateWith(request);
contact.ModifiedDate = DateTime.UtcNow;
return new UpdateContactResponse();
}
}
public static class ContactServiceExtensions // DRY reusable logic used in Services and Validators
{
public static int GetUserId(this Service service) => int.Parse(service.GetSession().UserAuthId);
public static bool IsValidColor(this string color) => !string.IsNullOrEmpty(color) &&
(color.FirstCharEquals('#')
? int.TryParse(color.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _)
: Color.FromName(color).IsNamedColor);
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel;
using ServiceStack;
namespace MyApp
{
namespace Data // DB Models
{
using ServiceModel.Types;
public class Contact // Data Model
{
public int Id { get; set; }
public int UserAuthId { get; set; }
public Title Title { get; set; }
public string Name { get; set; }
public string Color { get; set; }
public FilmGenres[] FilmGenres { get; set; }
public int Age { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime ModifiedDate { get; set; }
}
}
namespace ServiceModel // Request/Response DTOs
{
using Types;
[Route("/contacts", "GET")]
public class GetContacts : IReturn<GetContactsResponse> {}
public class GetContactsResponse
{
public List<Contact> Results { get; set; }
public ResponseStatus ResponseStatus { get; set; }
}
[Route("/contacts/{Id}", "GET")]
public class GetContact : IReturn<GetContactResponse >
{
public int Id { get; set; }
}
public class GetContactResponse
{
public Contact Result { get; set; }
public ResponseStatus ResponseStatus { get; set; }
}
[Route("/contacts", "POST")]
public class CreateContact : IReturn<CreateContactResponse>
{
public Title Title { get; set; }
public string Name { get; set; }
public string Color { get; set; }
public FilmGenres[] FilmGenres { get; set; }
public int Age { get; set; }
public bool Agree { get; set; }
}
public class CreateContactResponse
{
public Contact Result { get; set; }
public ResponseStatus ResponseStatus { get; set; }
}
[Route("/contacts/{Id}", "POST PUT")]
public class UpdateContact : IReturn<UpdateContactResponse>
{
public int Id { get; set; }
public Title Title { get; set; }
public string Name { get; set; }
public string Color { get; set; }
public FilmGenres[] FilmGenres { get; set; }
public int Age { get; set; }
}
public class UpdateContactResponse
{
public ResponseStatus ResponseStatus { get; set; }
}
[Route("/contacts/{Id}", "DELETE")]
[Route("/contacts/{Id}/delete", "POST")] // more accessible from HTML
public class DeleteContact : IReturnVoid
{
public int Id { get; set; }
}
namespace Types // DTO Types
{
public class Contact
{
public int Id { get; set; }
public int UserAuthId { get; set; }
public Title Title { get; set; }
public string Name { get; set; }
public string Color { get; set; }
public FilmGenres[] FilmGenres { get; set; }
public int Age { get; set; }
}
public enum Title
{
Unspecified=0,
[Description("Mr.")] Mr,
[Description("Mrs.")] Mrs,
[Description("Miss.")] Miss
}
public enum FilmGenres
{
Action,
Adventure,
Comedy,
Drama,
}
}
}
}
{{ 'requires-auth' | partial }}
{{ { id } | sendToGateway('GetContact', {catchError:'ex'}) | assignTo: response }}
{{#with response.Result}}
<h3 class="py-2">Update Contact</h3>
<form action="/contacts/{{Id}}" method="post" class="col-lg-4">
<div class="form-group" data-validation-summary="title,name,color,filmGenres,age"></div>
<div class="form-group">
<div class="form-check">
{{#each contactTitles}}
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="title-{{Key}}" name="title" value="{{Key}}" class="custom-control-input">
<label class="custom-control-label" for="title-{{Key}}">{{Value}}</label>
</div>
{{/each}}
</div>
</div>
<div class="form-group">
<label for="name">Full Name</label>
<input class="form-control" id="name" name="name" type="text" placeholder="Name">
<small id="name-help" class="text-muted">Your first and last name</small>
</div>
<div class="form-group">
<label class="form-label" for="color">Favorite color</label>
<select id="color" name="color" class="col-4 form-control">
<option value=""></option>
{{#each contactColors}}
<option value="{{Key}}">{{Value}}</option>
{{/each}}
</select>
</div>
<div class="form-group">
<label class="form-check-label">Favorite Film Genres</label>
<div class="form-check">
{{#each contactGenres}}
<div class="custom-control custom-checkbox">
<input type="checkbox" id="filmGenres-{{it}}" name="filmGenres" value="{{it}}" class="form-check-input">
<label class="form-check-label" for="filmGenres-{{it}}">{{it}}</label>
</div>
{{/each}}
</div>
</div>
<div class="form-group">
<input class="form-control col-3" name="age" type="number" min="3" placeholder="Age">
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit">Update Contact</button>
<a href="/contacts">cancel</a>
</div>
</form>
<script>var CONTACT = {{response.Result | json}};</script>
{{#raw}}
<script>!window["@servicestack/client"] && document.write(unescape('%3Cscript src="https://unpkg.com/@servicestack/client/dist/servicestack-client.umd.js"%3E%3C/script%3E'));</script>
<script>
(function(_){
const form = document.querySelector("form");
_.bootstrapForm(form,{
model: CONTACT,
success: function () {
location.href = '/contacts';
}
});
})(window["@servicestack/client"]);
</script>
{{/raw}}
{{else if ex}}
<div class="col-6">
<div class="alert alert-warning">{{ex.Message}}</div>
<p><a href="/contacts">&lt; back</a></p>
</div>
{{/with}}
{{ redirectIfNotAuthenticated }}
<div style="text-align:right" class="mr-2">
<small class="text-muted">
{{ userSession.DisplayName }}
| <a href="/auth/logout?continue=/">Sign Out</a>
</small>
</div>
{{ 'requires-auth' | partial }}
<h3 class="py-2">Add new Contact</h3>
<form action="/contacts" method="post" class="col-lg-4">
<div class="form-group" data-validation-summary="title,name,color,filmGenres,age,agree"></div>
<div class="form-group">
<div class="form-check">
{{#each contactTitles}}
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="title-{{Key}}" name="title" value="{{Key}}" class="custom-control-input">
<label class="custom-control-label" for="title-{{Key}}">{{Value}}</label>
</div>
{{/each}}
</div>
</div>
<div class="form-group">
<label for="name">Full Name</label>
<input class="form-control" id="name" name="name" type="text" placeholder="Name">
<small id="name-help" class="text-muted">Your first and last name</small>
</div>
<div class="form-group">
<label class="form-label" for="color">Favorite color</label>
<select id="color" name="color" class="col-6 form-control">
<option value=""></option>
{{#each contactColors}}
<option value="{{Key}}">{{Value}}</option>
{{/each}}
</select>
</div>
<div class="form-group">
<label class="form-check-label">Favorite Film Genres</label>
<div class="form-check">
{{#each contactGenres}}
<div class="custom-control custom-checkbox">
<input type="checkbox" id="filmGenres-{{it}}" name="filmGenres" value="{{it}}" class="form-check-input">
<label class="form-check-label" for="filmGenres-{{it}}">{{it}}</label>
</div>
{{/each}}
</div>
</div>
<div class="form-group">
<input class="form-control col-4" name="age" type="number" min="13" placeholder="Age">
</div>
<div class="form-group">
<div class="form-check">
<input class=" form-check-input" id="agree" name="agree" type="checkbox" value="true">
<label class="form-check-label" for="agree">Agree to terms and conditions</label>
</div>
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit">Add Contact</button>
<a class="btn outline-secondary" data-click="reset">reset</a>
</div>
</form>
<table id="results"></table>
{{ htmlError }}
<script>var CONTACTS = {{ sendToGateway('GetContacts') | map => it.Results | json }};</script>
{{#raw appendTo scripts}}
<script>!window["@servicestack/client"] && document.write(unescape('%3Cscript src="https://unpkg.com/@servicestack/client/dist/servicestack-client.umd.js"%3E%3C/script%3E'));</script>
<script>
(function(_){
// Copied from dtos.ts generated by https://docs.servicestack.net/typescript-add-servicestack-reference
// @Route("/contacts", "GET")
var GetContacts = /** @class */ (function () {
function GetContacts(init) {
Object.assign(this, init);
}
GetContacts.prototype.createResponse = function () { return new GetContactsResponse(); };
GetContacts.prototype.getTypeName = function () { return 'GetContacts'; };
return GetContacts;
}());
var GetContactsResponse = /** @class */ (function () {
function GetContactsResponse(init) {
Object.assign(this, init);
}
return GetContactsResponse;
}());
// @Route("/contacts/{Id}", "GET")
var GetContact = /** @class */ (function () {
function GetContact(init) {
Object.assign(this, init);
}
GetContact.prototype.createResponse = function () { return new GetContactResponse(); };
GetContact.prototype.getTypeName = function () { return 'GetContact'; };
return GetContact;
}());
// @Route("/contacts/{Id}", "DELETE")
// @Route("/contacts/{Id}/delete", "POST")
var DeleteContact = /** @class */ (function () {
function DeleteContact(init) {
Object.assign(this, init);
}
DeleteContact.prototype.createResponse = function () { };
DeleteContact.prototype.getTypeName = function () { return 'DeleteContact'; };
return DeleteContact;
}());
var client = new _.JsonServiceClient();
var form = document.querySelector("form");
_.bootstrapForm(form,{
success: function (r) {
form.reset();
CONTACTS.push(r.result);
render();
}
});
_.bindHandlers({
reset: function() {
document.querySelector('form').reset();
},
deleteContact: function(id) {
if (!confirm('Are you sure?'))
return;
client.delete(new DeleteContact({ id:id }))
.then(function() {
client.get(new GetContacts())
.then(function(r){
CONTACTS = r.results;
render();
})
});
}
});
function contactRow(contact)
{
return '<tr style="background:' + contact.color + '">' +
'<td class="p-2">' + contact.title + ' ' + contact.name + ' (' + contact.age + ')</td>' +
'<td class="p-2"><a href="/contacts/' + contact.id + '/edit">edit</a></td>' +
'<td class="p-2"><button class="btn btn-sm btn-primary" data-click="deleteContact:' + contact.id + '">delete</button></td>' +
'</tr>';
}
function render() {
var sb = "";
if (CONTACTS.length > 0) {
for (let i=0; i<CONTACTS.length; i++) {
sb += contactRow(CONTACTS[i])
}
} else {
sb = "<tr><td>There are no contacts.</td></tr>";
}
document.querySelector("#results").innerHTML = '<tbody>' + sb + '</tbody>';
}
render();
})(window["@servicestack/client"]);
</script>
{{/raw}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment