Skip to content

Instantly share code, notes, and snippets.

@frankradocaj
Last active November 15, 2017 05:22
Show Gist options
  • Save frankradocaj/683ef42ab402bb51ceba9ac3c859e8c3 to your computer and use it in GitHub Desktop.
Save frankradocaj/683ef42ab402bb51ceba9ac3c859e8c3 to your computer and use it in GitHub Desktop.
Simplifying ASP.Net Controllers
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using System.Net;
using Newtonsoft.Json.Serialization;
using System.Text;
namespace WebApplication1
{
public interface IServiceResponse
{
object Response { get; }
}
public interface IServiceSuccess : IServiceResponse { }
public interface IServiceFailure : IServiceResponse
{
ServiceFailureResponse.ErrorResponse Error { get; }
}
public class ServiceResponse
{
public static ServiceSuccessResponse Success()
{
return new ServiceSuccessResponse
{
Response = new ServiceSuccessResponse.NoResponse()
};
}
public static ServiceSuccessResponse Success<T>(T response)
{
return new ServiceSuccessResponse
{
Response = response
};
}
public static ServiceFailureResponse Failure(string errorCode)
{
return new ServiceFailureResponse
{
Error = new ServiceFailureResponse.ErrorResponse
{
ErrorCode = errorCode
}
};
}
}
public class ServiceSuccessResponse : IServiceSuccess
{
public object Response { get; set; }
public class NoResponse { }
}
public class ServiceFailureResponse : IServiceFailure
{
object IServiceResponse.Response => Error;
public ErrorResponse Error { get; set; }
public class ErrorResponse
{
public string ErrorCode { get; set; }
}
}
public interface IPagedList
{
int Page { get; }
int PageSize { get; }
int TotalResultCount { get; }
IEnumerable<object> Items { get; }
}
public interface IPagedList<T> : IPagedList
{
new IEnumerable<T> Items { get; }
}
public sealed class PagedList<T> : IPagedList<T>
{
public IEnumerable<T> Items { get; set; }
IEnumerable<object> IPagedList.Items => Items.OfType<object>();
public int Page { get; }
public int PageSize { get; }
public int TotalResultCount { get; }
}
public abstract class FrankController : Controller
{
public abstract HttpStatusCode GetErrorHttpStatusCode(string errorCode);
public JsonResult ApiResult(IServiceResponse response, string pagingPath, HttpStatusCode? successResponseHttpStatusCodeOverride = null)
{
JsonResult jsonResult;
if (response.Response is IPagedList pagedList)
{
if (string.IsNullOrWhiteSpace(pagingPath))
{
throw new ApplicationException("Must specify a pagePath for a PagedList response");
}
SetPagingHeaders(pagedList, pagingPath);
jsonResult = new JsonResult(pagedList.Items);
}
else
{
jsonResult = new JsonResult(response.Response);
}
HttpStatusCode httpStatusCode;
if (response is IServiceFailure failureResponse)
{
httpStatusCode = GetErrorHttpStatusCode(failureResponse.Error.ErrorCode);
}
else
{
httpStatusCode = successResponseHttpStatusCodeOverride ?? HttpStatusCode.OK; // Default
}
jsonResult.StatusCode = (int)httpStatusCode;
jsonResult.SerializerSettings = new Newtonsoft.Json.JsonSerializerSettings
{
Formatting = Newtonsoft.Json.Formatting.Indented,
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
return jsonResult;
}
public JsonResult ApiResult(IServiceResponse response, HttpStatusCode? successResponseHttpStatusCodeOverride = null)
{
return ApiResult(response, null, successResponseHttpStatusCodeOverride);
}
/// <summary>
/// Content-Range is structured as such: "0-9/76" or */0 for no results.
///
/// If there are additional results - ie: another page, the "Link"
void SetPagingHeaders(IPagedList pagedList, string path)
{
if (pagedList.TotalResultCount == 0)
{
Response.Headers["Content-Range"] = "*/0";
}
else
{
if (pagedList.Page * pagedList.PageSize >= pagedList.TotalResultCount)
{
Response.Headers["Content-Range"] = ContentRangeGenerator(pagedList);
}
else
{
Response.Headers["Content-Range"] = ContentRangeGenerator(pagedList);
Response.Headers["Link"] = LinkGenerator(pagedList, path);
}
}
}
string LinkGenerator(IPagedList pagedList, string path)
{
const string ApiRootUri = "";
var strFormat = @"<{0}{1}?page={2}&count={3}>; rel=""{4}""";
var str = new StringBuilder();
// First page
str.Append(string.Format(strFormat, ApiRootUri, path, 1, pagedList.PageSize, "first"));
// Previous page
if (pagedList.Page > 1)
{
str.Append(",");
str.Append(string.Format(strFormat, ApiRootUri, path, pagedList.Page * pagedList.PageSize, pagedList.PageSize, "prev"));
}
// Next page
if (pagedList.Page * pagedList.PageSize < pagedList.TotalResultCount)
{
str.Append(",");
str.Append(string.Format(strFormat, ApiRootUri, path, pagedList.Page + pagedList.PageSize, pagedList.PageSize, "next"));
}
// Last page
int lastPage = (pagedList.TotalResultCount % pagedList.PageSize) == 0 ? pagedList.TotalResultCount - pagedList.PageSize : pagedList.TotalResultCount - (pagedList.TotalResultCount % pagedList.PageSize);
str.Append(",");
str.Append(string.Format(strFormat, ApiRootUri, path, lastPage, pagedList.PageSize, "last"));
return str.ToString();
}
string ContentRangeGenerator(IPagedList pagedList)
{
var pageEnd = pagedList.Page * pagedList.PageSize;
var pageStart = pageEnd - pagedList.PageSize + 1;
return string.Format("{0}-{1}/{2}", pageStart, pageEnd < pagedList.TotalResultCount ? pageEnd : pagedList.TotalResultCount, pagedList.TotalResultCount);
}
}
}
using Microsoft.AspNetCore.Mvc;
using System.Net;
namespace WebApplication1
{
[Route("api/values")]
public class ValuesController : FrankController
{
readonly IValuesService _valuesService;
public ValuesController(IValuesService valuesService)
{
_valuesService = valuesService;
}
public override HttpStatusCode GetErrorHttpStatusCode(string errorCode)
{
switch (errorCode)
{
case ValuesServiceErrorCodes.INVALID_CRITERIA: return HttpStatusCode.BadRequest;
default: return HttpStatusCode.BadRequest;
}
}
// GET api/values
[HttpGet]
public JsonResult Get(ValuesQuery request) => ApiResult(_valuesService.SearchValues(request));
// GET api/values/{id:guid}
[HttpGet("{id}")]
public JsonResult Get(ValueRequest request) => ApiResult(_valuesService.GetValue(request));
// POST api/values
[HttpPost]
public void Post([FromBody][FromRoute]AddValueRequest request) => ApiResult(_valuesService.AddValue(request));
}
}
using System;
namespace WebApplication1.Controllers
{
public interface IValuesService
{
IServiceResponse SearchValues(ValuesQuery request);
IServiceResponse GetValue(ValueRequest request);
IServiceResponse AddValue(AddValueRequest request);
}
public static class ValuesServiceErrorCodes
{
public const string INVALID_CRITERIA = "invalid_criteria";
}
public class ValuesService : IValuesService
{
public IServiceResponse SearchValues(ValuesQuery request)
{
return ServiceResponse.Success(new PagedList<object>
{
Items = new object[] { }
});
}
public IServiceResponse GetValue(ValueRequest request)
{
if (request.Id == new Guid("e551a7b7-5681-4b9b-b470-dca7062731f0"))
{
return ServiceResponse.Success(new
{
Id = new Guid("e551a7b7-5681-4b9b-b470-dca7062731f0"),
Name = "Frank"
});
}
return ServiceResponse.Failure(ValuesServiceErrorCodes.INVALID_CRITERIA);
}
public IServiceResponse AddValue(AddValueRequest request)
{
return ServiceResponse.Success(new
{
Id = new Guid("e551a7b7-5681-4b9b-b470-dca7062731f0")
});
}
}
public class ValuesQuery { }
public class ValueRequest
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class AddValueRequest
{
public string Name { get; set; }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment