Skip to content

Instantly share code, notes, and snippets.

@JaimeStill
Last active March 18, 2022 18:23
Show Gist options
  • Save JaimeStill/e722643f15bd3f0ed30168c4cfd0c287 to your computer and use it in GitHub Desktop.
Save JaimeStill/e722643f15bd3f0ed30168c4cfd0c287 to your computer and use it in GitHub Desktop.
.NET + Angular distributed reactive architecture + stack updates

Distributed Reactive Architecture

[assembly:SupportedOSPlatform("windows")]
namespace App.Auth;
using System.Runtime.Versioning;
using Microsoft.EntityFrameworkCore;
using App.Core;
using App.Data;
using App.Data.Extensions;
using App.Data.Models;
using App.Identity;
public static class AuthorizationExtensions
{
static AppException Denied(this IUserProvider provider, string message = "")
{
if (string.IsNullOrEmpty(message))
return new AppException(
$"{provider.CurrentUser.SamAccountName} is not authorized to access this resource",
ExceptionType.Authorization
);
else
return new AppException(message, ExceptionType.Authorization);
}
#region Admin Authorization
public static async Task<T> AuthorizeAdmin<T>(
this AppDbContext db,
IUserProvider provider,
Func<AppDbContext, Task<T>> exec
)
{
if (await db.ValidateAdmin(provider))
return await exec(db);
else
throw provider.Denied($"{provider.CurrentUser.SamAccountName} is not an administrator");
}
public static async Task AuthorizeAdmin(
this AppDbContext db,
IUserProvider provider,
Func<AppDbContext, Task> exec
)
{
if (await db.ValidateAdmin(provider))
await exec(db);
else
throw provider.Denied($"{provider.CurrentUser.SamAccountName} is not an administrator");
}
public static async Task<bool> ValidateAdmin(this AppDbContext db, IUserProvider provider) =>
await db.Users
.AnyAsync(x =>
x.Guid == provider.CurrentUser.Guid.Value
&& x.IsAdmin
);
#endregion
#region Staffing
#region Process State
public static async Task<ProcessState> GetProcessState(this AppDbContext db, IUserProvider provider, int id) =>
new ProcessState
{
Process = await db.GetProcess(id),
Permissions = await db.GetProcessAuth(provider, id),
IsCurrent = await db.IsCurrentProcess(id),
IsReviewed = await db.IsProcessReviewed(id),
Reviews = await db.GetProcessReviews(id)
};
static async Task<bool> IsCurrentProcess(this AppDbContext db, int processId)
{
var process = await db.GetProcess(processId);
var current = await db.Processes
.Where(x =>
x.WorkflowId == process.WorkflowId
&& (
!x.IsApproved.HasValue
|| x.IsApproved.Value == false
)
)
.OrderBy(x => x.Index)
.FirstOrDefaultAsync();
return current is not null && process.Id == current.Id;
}
static async Task<bool> IsProcessReviewed(this AppDbContext db, int processId) =>
await db.ProcessReviews
.Where(x => x.ProcessId == processId)
.AllAsync(x => x.Concur.HasValue);
static async Task<ProcessAuth> GetProcessAuth(this AppDbContext db, IUserProvider provider, int processId)
{
var process = await db.Processes
.Include(x => x.Workflow)
.FirstOrDefaultAsync(x => x.Id == processId);
var userId = await db.GetUserIdByGuid(provider.CurrentUser.Guid.Value);
var roleIds = await db.UserRoles
.Where(x => x.UserId == userId)
.Select(x => x.RoleId)
.ToListAsync();
var reviewRoles = await db.ProcessReviews
.Where(x => x.ProcessId == processId)
.Select(x => x.RoleId)
.ToListAsync();
var contributor = roleIds.Any(x =>
reviewRoles.Contains(x)
|| x == process.RoleId
);
return new ProcessAuth
{
Approver = roleIds.Contains(process.RoleId),
Contributor = contributor,
RoleIds = roleIds
};
}
#endregion
public static async Task<bool> ValidateRole(this AppDbContext db, IUserProvider provider, int roleId) =>
await db.UserRoles
.AnyAsync(x =>
x.User.Guid == provider.CurrentUser.Guid.Value
&& x.RoleId == roleId
);
#endregion
}
namespace App.Core.ApiQuery;
using Microsoft.EntityFrameworkCore;
public class QueryContainer<T>
{
const int DEFAULT_PAGE_SIZE = 20;
private IQueryable<T> queryable;
private readonly QueryOptions options;
public IQueryable<T> Queryable => queryable;
public QueryOptions Options => options;
int GeneratePage(string page, int d) =>
int.TryParse(page, out int _page)
? _page
: d;
bool GenerateSortDirection(string sort)
{
var split = sort.Split('_');
if (split.Length > 1)
return split[1].ToLower() == "desc";
return false;
}
public QueryContainer(IQueryable<T> queryable, string page, string pageSize, string search, string sort)
{
options = new QueryOptions
{
Page = string.IsNullOrWhiteSpace(page)
? 1
: GeneratePage(page, 1),
PageSize = string.IsNullOrWhiteSpace(pageSize)
? DEFAULT_PAGE_SIZE
: GeneratePage(pageSize, DEFAULT_PAGE_SIZE),
Search = search,
SortDescending = !string.IsNullOrWhiteSpace(sort) && GenerateSortDirection(sort),
SortProperty = string.IsNullOrWhiteSpace(sort)
? "Id"
: sort.Split('_')[0]
};
this.queryable = string.IsNullOrWhiteSpace(options.SortProperty)
? queryable
: queryable.ApplySorting(options);
}
public async Task<QueryResult<T>> Query(Func<IQueryable<T>, string, IQueryable<T>> search)
{
if (!string.IsNullOrEmpty(Options.Search))
queryable = search(Queryable, Options.Search);
dynamic dynamicQueryable = Queryable;
var totalCount = await EntityFrameworkQueryableExtensions.CountAsync(dynamicQueryable);
if (totalCount <= ((Options.Page - 1) * Options.PageSize))
{
Options.Page = (int)System.Math.Ceiling((decimal)totalCount / Options.PageSize);
Options.Page = Options.Page == 0
? 1
: Options.Page;
}
return new QueryResult<T>
{
Page = Options.Page,
PageSize = Options.PageSize,
TotalCount = totalCount,
Data = await Queryable
.Skip((Options.Page - 1) * Options.PageSize)
.Take(Options.PageSize)
.ToListAsync()
};
}
}
public static class QueryExtensions
{
public static QueryResult<C> Cast<T,C>(this QueryResult<T> result, List<C> data) => new QueryResult<C>
{
Data = data,
Page = result.Page,
PageSize = result.PageSize,
TotalCount = result.TotalCount
};
}
namespace App.Core.ApiQuery;
using System.Linq.Expressions;
using System.Reflection;
public static class QueryableProcessor
{
public static IQueryable<T> ApplySorting<T>(this IQueryable<T> queryable, QueryOptions options)
{
if (queryable == null)
throw new ArgumentNullException(nameof(queryable));
if (options == null)
throw new ArgumentNullException(nameof(options));
if (string.IsNullOrWhiteSpace(options.SortProperty))
return queryable;
var orderMethodName = options.SortDescending
? nameof(Queryable.OrderByDescending)
: nameof(Queryable.OrderBy);
var result = ApplySorting(queryable, orderMethodName, options.SortProperty);
return result;
}
static IQueryable<T> ApplySorting<T>(IQueryable<T> queryable, string sortMethodName, string propertyName)
{
var (declaringType, property) = GetPropertyInfoRecursively(queryable, propertyName);
if (declaringType == null || property == null)
return queryable;
var orderByExp = CreateExpression(declaringType, propertyName);
if (orderByExp == null)
return queryable;
queryable = queryable.WrapInNullChecksIfAccessingNestedProperties(propertyName);
var wrappedExpression = Expression.Call(
typeof(Queryable),
sortMethodName,
new[] { declaringType, property.PropertyType },
queryable.Expression,
Expression.Quote(orderByExp)
);
var result = queryable.Provider.CreateQuery(wrappedExpression);
return result.Cast<T>();
}
static (Type declaringType, PropertyInfo property) GetPropertyInfoRecursively<T>(this IQueryable<T> queryable, string propName)
{
var nameParts = propName.Split('.');
if (nameParts.Length == 1)
{
var property =
queryable.ElementType.GetTypeInfo().GetProperty(CamelizeString(propName))
?? queryable.ElementType.GetTypeInfo().GetProperty(propName);
return (property?.DeclaringType, property);
}
var propertyInfo =
queryable.ElementType.GetTypeInfo().GetProperty(CamelizeString(nameParts[0]))
?? queryable.ElementType.GetTypeInfo().GetProperty(nameParts[0]);
if (propertyInfo == null)
return (null, null);
var originalDeclaringType = propertyInfo.DeclaringType;
for (var i = 1; i < nameParts.Length; i++)
{
propertyInfo =
propertyInfo.PropertyType.GetProperty(CamelizeString(nameParts[i]))
?? propertyInfo.PropertyType.GetProperty(nameParts[i]);
if (propertyInfo == null)
return (null, null);
}
return (originalDeclaringType, propertyInfo);
}
static LambdaExpression CreateExpression(Type type, string propertyName)
{
var param = Expression.Parameter(type, "v");
Expression body = param;
foreach (var member in propertyName.Split('.'))
body = Expression.PropertyOrField(body, CamelizeString(member))
?? Expression.PropertyOrField(body, member);
return Expression.Lambda(body, param);
}
static IQueryable<T> WrapInNullChecksIfAccessingNestedProperties<T>(this IQueryable<T> queryable, string propertyName)
{
var members = propertyName.Split('.');
if (members.Length == 1)
return queryable;
for (var i = 0; i < members.Length - 1; i++)
{
var member = members[i];
var param = Expression.Parameter(typeof(T), "v");
Expression body = param;
for (var j = 0; j <= i; j++)
body = Expression.PropertyOrField(body, CamelizeString(members[j]))
?? Expression.PropertyOrField(body, members[j]);
var memberPath = members
.TakeWhile((mem, index) => index <= i)
.Aggregate((c, n) => c + "." + n);
var notNullExpression = Expression.NotEqual(body, Expression.Constant(null));
var notNullLambda = Expression.Lambda(notNullExpression, param);
var whereMethodName = nameof(Queryable.Where);
var nullCheckExpression = Expression.Call(
typeof(Queryable),
whereMethodName,
new[] { typeof(T) },
queryable.Expression,
Expression.Quote(notNullLambda)
);
queryable.Provider.CreateQuery(nullCheckExpression);
}
return queryable;
}
static string CamelizeString(string camelCase) =>
string.Concat(camelCase[..1].ToUpperInvariant(), camelCase.AsSpan(1));
}
namespace App.Core.ApiQuery;
public class QueryOptions
{
public int Page { get; set; } = 1;
public int PageSize { get; set; }
public string Search { get; set; }
public string SortProperty { get; set; }
public bool SortDescending { get; set; }
}
namespace App.Core.ApiQuery;
public class QueryResult<T>
{
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalCount { get; set; }
public List<T> Data { get; set; }
}
namespace App.Core.Extensions;
using App.Core.Logging;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System.Text;
using System.Text.RegularExpressions;
public static class CoreExtensions
{
private static readonly string jsTimeFormat = "yyyy-MM-ddTHH:mm:ss.fffZ";
private static readonly string urlPattern = "[^a-zA-Z0-9-.]";
public static string UpdateDateTime(
this DateTime date,
int years = 0,
int months = 0,
int days = 0,
int hours = 0,
int minutes = 0
) => date
.AddYears(years)
.AddMonths(months)
.AddDays(days)
.AddHours(hours)
.AddMinutes(minutes)
.ToString(jsTimeFormat);
public static string ToJsDateString(this DateTime date) => date.ToString(jsTimeFormat);
public static string GetModuleBase(this IConfiguration config, string module) =>
config.GetSection("AppModules")
.GetValue<string>(module.ToLower());
public static IQueryable<T> SetupSearch<T>(
this IQueryable<T> values,
string search,
Func<IQueryable<T>, string, IQueryable<T>> action,
char split = '|'
)
{
if (search.Contains(split))
{
var searches = search.Split(split);
foreach (var s in searches)
{
values = action(values, s.Trim());
}
return values;
}
else
return action(values, search);
}
public static string SerializeToJson<T>(
this T data,
ReferenceLoopHandling referenceLoopHandling = ReferenceLoopHandling.Ignore,
MetadataPropertyHandling metadataPropertyHandling = MetadataPropertyHandling.Ignore,
Formatting formatting = Formatting.Indented,
NullValueHandling nullValueHandling = NullValueHandling.Ignore
)
{
var settings = new JsonSerializerSettings
{
ReferenceLoopHandling = referenceLoopHandling,
MetadataPropertyHandling = metadataPropertyHandling,
Formatting = formatting,
NullValueHandling = nullValueHandling,
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
return JsonConvert.SerializeObject(data, settings);
}
public static object DeserializeJson(
this Type type,
string data
) => JsonConvert.DeserializeObject(data, type);
public static void EnsureDirectoryExists(this string path)
{
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
}
public static string UrlEncode(this string url) => url.UrlEncode(urlPattern, "-");
public static string UrlEncode(this string url, string pattern, string replace = "")
{
var friendlyUrl = Regex.Replace(url, @"\s", "-").ToLower();
friendlyUrl = Regex.Replace(friendlyUrl, pattern, replace);
return friendlyUrl;
}
public static string GetExceptionChain(this Exception ex)
{
var message = new StringBuilder(ex.Message);
if (ex.InnerException != null)
{
message.AppendLine();
message.AppendLine(GetExceptionChain(ex.InnerException));
}
return message.ToString();
}
public static void HandleError(this IApplicationBuilder app, LogProvider logger)
{
app.Run(async context =>
{
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null)
{
var ex = error.Error;
if (ex is AppException)
{
switch (((AppException)ex).ExceptionType)
{
case ExceptionType.Authorization:
await logger.CreateLog(context, ex, "auth");
break;
case ExceptionType.Validation:
break;
default:
await logger.CreateLog(context, ex);
break;
}
await context.SendErrorResponse(ex);
}
else
{
await logger.CreateLog(context, ex);
await context.SendErrorResponse(ex);
}
}
});
}
static async Task SendErrorResponse(this HttpContext context, Exception ex)
{
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(ex.GetExceptionChain(), Encoding.UTF8);
}
}
[assembly:SupportedOSPlatform("windows")]
namespace Argo.Data;
using System.Runtime.Versioning;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using App.Data.Entities;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Notification> Notifications { get; set; }
public DbSet<Organization> Organizations { get; set; }
public DbSet<OrganizationImage> OrganizationImages { get; set; }
public DbSet<Process> Processes { get; set; }
public DbSet<ProcessT> ProcessTs { get; set; }
public DbSet<ProcessNote> ProcessNotes { get; set; }
public DbSet<ProcessReview> ProcessReviews { get; set; }
public DbSet<ProcessReviewT> ProcessReviewTs { get; set; }
public DbSet<ProcessUpload> ProcessUploads { get; set; }
public DbSet<Role> Roles { get; set; }
public DbSet<Upload> Uploads { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<UserRole> UserRoles { get; set; }
public DbSet<Workflow> Workflows { get; set; }
public DbSet<WorkflowT> WorkflowTs { get; set; }
public DbSet<WorkflowItem> WorkflowItems { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
#region Default Values
modelBuilder
.Entity<User>()
.Property(user => user.DefaultPageSize)
.HasDefaultValue(20);
#endregion
#region One-to-One Mappings
modelBuilder
.Entity<OrganizationImage>()
.HasOne(x => x.Organization)
.WithOne(x => x.OrganizationImage)
.HasForeignKey<OrganizationImage>(x => x.OrganizationId)
.IsRequired(false);
#endregion
#region Non-Standard Foreign Key Mappings
modelBuilder
.Entity<ProcessNote>()
.HasOne(x => x.Process)
.WithMany(x => x.Notes)
.HasForeignKey(x => x.ProcessId);
modelBuilder
.Entity<ProcessReview>()
.HasOne(x => x.Process)
.WithMany(x => x.Reviews)
.HasForeignKey(x => x.ProcessId);
modelBuilder
.Entity<ProcessReview>()
.HasOne(x => x.Role)
.WithMany(x => x.Reviews)
.HasForeignKey(x => x.RoleId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder
.Entity<ProcessReviewT>()
.HasOne(x => x.ProcessT)
.WithMany(x => x.ReviewTs)
.HasForeignKey(x => x.ProcessTId);
modelBuilder
.Entity<ProcessReviewT>()
.HasOne(x => x.Role)
.WithMany(x => x.ReviewTs)
.HasForeignKey(x => x.RoleId)
.OnDelete(DeleteBehavior.Restrict);
#endregion
#region Optional Foreign Key Mappings
modelBuilder
.Entity<Process>()
.HasOne(x => x.User)
.WithMany(x => x.Processes)
.HasForeignKey(x => x.UserId)
.IsRequired(false);
#endregion
#region Enum Config
/*
How to register enum to string conversion
modelBuilder
.Entity<EntityType>()
.Propert(x => x.EnumProperty)
.HasConversion(new EnumToStringConverter<EnumType>());
*/
#endregion
#region Inheritance Structures
modelBuilder
.Entity<Upload>()
.HasDiscriminator(x => x.Type)
.HasValue<Upload>("upload")
.HasValue<OrganizationImage>("organization-image")
.HasValue<ProcessUpload>("process-upload");
modelBuilder
.Entity<WorkflowItem>()
.HasDiscriminator(x => x.Type)
.HasValue<WorkflowItem>("item");
#endregion
modelBuilder
.Model
.GetEntityTypes()
.Where(x => x.BaseType == null)
.ToList()
.ForEach(x =>
{
modelBuilder
.Entity(x.Name)
.ToTable(x.Name.Split('.').Last());
});
}
}
namespace App.Data.Entities;
public class Notification
{
public int Id { get; set; }
public int UserId { get; set; }
public string Message { get; set; }
public string Url { get; set; }
public string PushDate { get; set; }
public bool IsAlert { get; set; }
public bool IsRead { get; set; }
public User User { get; set; }
}
namespace App.Data.Entities;
public class Organization
{
public int Id { get; set; }
public string Name { get; set; }
/* One to One */
public OrganizationImage OrganizationImage { get; set; }
public IEnumerable<Workflow> Workflows { get; set; }
public IEnumerable<WorkflowT> WorkflowTs { get; set; }
}
namespace App.Data.Entities;
public class Process
{
public int Id { get; set; }
public int WorkflowId { get; set; }
public int RoleId { get; set; }
public int? UserId { get; set; }
public int Index { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool? IsApproved { get; set; }
public bool? IsRejected { get; set; }
public DateTime? ActionDate { get; set; }
public Workflow Workflow { get; set; }
public StaffingRole Role { get; set; }
public User User { get; set; }
public IEnumerable<ProcessNote> Notes { get; set; }
public IEnumerable<ProcessReview> Reviews { get; set; }
public IEnumerable<ProcessUpload> Uploads { get; set; }
}
namespace App.Data.Entities;
public class ProcessNote
{
public int Id { get; set; }
public int ProcessId { get; set; }
public int UserId { get; set; }
public DateTime DateCreated { get; set; }
public string Value { get; set; }
public Process Process { get; set; }
public User User { get; set; }
}
namespace App.Data.Entities;
public class ProcessReview
{
public int Id { get; set; }
public int ProcessId { get; set; }
public int RoleId { get; set; }
public int? UserId { get; set; }
public string Name { get; set; }
public bool? Concur { get; set; }
public DateTime? ActionDate { get; set; }
public Process Process { get; set; }
public Role Role { get; set; }
public User User { get; set; }
}
namespace App.Data.Entities;
public class ProcessReviewT
{
public int Id { get; set; }
public int ProcessTId { get; set; }
public int RoleId { get; set; }
public string Name { get; set; }
public ProcessT ProcessT { get; set; }
public Role Role { get; set; }
}
namespace App.Data.Entities;
public class ProcessT
{
public int Id { get; set; }
public int RoleId { get; set; }
public int WorkflowTId { get; set; }
public int Index { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public Role Role { get; set; }
public WorkflowT WorkflowT { get; set; }
public IEnumerable<ProcessReviewT> ReviewTs { get; set; }
}
namespace App.Data.Entities;
public class Role
{
public int Id { get; set; }
public string Value { get; set; }
public string Description { get; set; }
public IEnumerable<Process> Processes { get; set; }
public IEnumerable<ProcessT> ProcessTs { get; set; }
public IEnumerable<ProcessReview> ProcessReviews { get; set; }
public IEnumerable<ProcessReviewT> ProcessReviewTs { get; set; }
public IEnumerable<UserRole> UserRoles { get; set; }
}
namespace App.Data.Entities;
public class Upload
{
public int Id { get; set; }
public string Type { get; set; }
public string Url { get; set; }
public string Path { get; set; }
public string File { get; set; }
public string Name { get; set; }
public string FileType { get; set; }
public long Size { get; set; }
}
public class OrganizationImage : Upload
{
public int OrganizationId { get; set; }
public Organization Organization { get; set; }
}
public class ProcessUpload : Upload
{
public int ProcessId { get; set; }
public Process Process { get; set; }
}
namespace App.Data.Entities;
public class User
{
public int Id { get; set; }
/*
Properties mapped from App.Identity.AdUser.cs
*/
public Guid Guid { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string DisplayName { get; set; }
public string EmailAddress { get; set; }
public string DistinguishedName { get; set; }
public string SamAccountName { get; set; }
public string UserPrincipalName { get; set; }
public string VoiceTelephoneNumber { get; set; }
public string SocketName { get; set; }
public int DefaultPageSize { get; set; }
public bool UseDarkTheme { get; set; }
public bool IsAdmin { get; set; }
public IEnumerable<Notification> Notifications { get; set; }
public IEnumerable<Process> Processes { get; set; }
public IEnumerable<ProcessNote> ProcessNotes { get; set; }
public IEnumerable<ProcessReview> ProcessReviews { get; set; }
}
namespace App.Data.Entities;
public class UserRole
{
public int Id { get; set; }
public int RoleId { get; set; }
public int UserId { get; set; }
public Role Role { get; set; }
public User User { get; set; }
}
namespace App.Data.Entities;
public class Workflow
{
public int Id { get; set; }
public int OrganizationId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool IsComplete { get; set; }
public IEnumerable<Process> Processes { get; set; }
public IEnumerable<WorkflowItem> WorkflowItems { get; set; }
}
namespace App.Data.Entities;
public class WorkflowItem
{
public int Id { get; set; }
public int WorkflowId { get; set; }
public string Type { get; set; }
public string Label { get; set; }
public Workflow Workflow { get; set; }
}
namespace App.Data.Entities;
public class WorkflowT
{
public int Id { get; set; }
public int OrganizationId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public Organization Organization { get; set; }
public IEnumerable<ProcessT> ProcessTs { get; set; }
}
namespace App.Data.Extensions;
using App.Core;
using App.Core.ApiQuery;
using App.Core.Extensions;
using App.Data.Entities;
using App.Identity;
using Microsoft.EntityFrameworkCore;
public static class IdentityExtensions
{
#region User
static IQueryable<User> Search(this IQueryable<User> users, string search) =>
users.Where(x =>
x.EmailAddress.ToLower().Contains(search.ToLower())
|| x.DisplayName.ToLower().Contains(search.ToLower())
|| x.DistinguishedName.ToLower().Contains(search.ToLower())
|| x.FirstName.ToLower().Contains(search.ToLower())
|| x.LastName.ToLower().Contains(search.ToLower())
|| x.MiddleName.ToLower().Contains(search.ToLower())
|| x.SamAccountName.ToLower().Contains(search.ToLower())
|| x.UserPrincipalName.ToLower().Contains(search.ToLower())
|| x.VoiceTelephoneNumber.ToLower().Contains(search.ToLower())
);
public static async Task<QueryResult<User>> QueryUsers(
this AppDbContext db,
string page,
string pageSize,
string search,
string sort
)
{
var container = new QueryContainer<User>(
db.Users,
page, pageSize, search, sort
);
return await container.Query((users, s) =>
users.SetupSearch(s, (values, term) =>
values.Search(term)
)
);
}
public static async Task<User> GetUser(this AppDbContext db, int id) =>
await db.Users
.FindAsync(id);
public static async Task<User> GetUserByGuid(this AppDbContext db, Guid guid) =>
await db.Users
.FirstOrDefaultAsync(x => x.Guid == guid);
public static async Task<int> GetUserIdByGuid(this AppDbContext db, Guid guid) =>
await db.Users
.Where(x => x.Guid == guid)
.Select(x => x.Id)
.FirstOrDefaultAsync();
public static async Task<User> SyncUser(this AdUser adUser, AppDbContext db)
{
var user = await db.GetUserByGuid(adUser.Guid.Value);
user = user == null
? await db.AddUser(adUser)
: await db.SyncUser(adUser, user);
return await db.GetUser(user.Id);
}
public static async Task<User> AddUser(this AppDbContext db, AdUser user)
{
User user = null;
if (await adUser.Validate(db))
{
user = adUser.ToUser();
await db.Users.AddAsync(user);
await db.SaveChangesAsync();
}
return user;
}
public static async Task UpdateUser(this AppDbContext db, User user)
{
db.Users.Update(user);
await db.SaveChangesAsync();
}
public static async Task RemoveUser(this AppDbContext db, User user)
{
db.Users.Remove(user);
await db.SaveChangesAsync();
}
public static async Task<bool> Validate(this AdUser user, AppDbContext db)
{
if (user is null)
throw new AppException("The AD User provided was null", ExceptionType.Validation);
if (user.Guid is null)
throw new AppException("The provided AD User does not have a GUID", ExceptionType.Validation);
var check = await db.GetUserByGuid(user.Guid.Value);
if (check is not null)
throw new AppException("The provided user already has an account", ExceptionType.Validation);
return true;
}
static async Task<User> SyncUser(this AppDbContext db, AdUser adUser, User user)
{
adUser.MergeUser(user);
await db.UpdateUser(user);
return user;
}
static User ToUser(this AdUser adUser)
{
var user = new User();
adUser.MergeUser(user);
return user;
};
static void MergeUser(this AdUser adUser, User user)
{
if (string.IsNullOrEmpty(user.DisplayName))
user.DisplayName = adUser.DisplayName;
user.DistinguishedName = adUser.DistinguishedName;
user.EmailAddress = adUser.EmailAddress;
user.FirstName = adUser.GivenName;
user.Guid = adUser.Guid.Value;
user.LastName = adUser.Surname;
user.MiddleName = adUser.MiddleName;
user.SamAccountName = adUser.SamAccountName;
user.UserPrincipalName = adUser.UserPrincipalName;
user.VoiceTelephoneNumber = adUser.VoiceTelephoneNumber;
user.SocketName = $@"{adUser.GetDomainPrefix()}\{adUser.SamAccountName}";
}
#endregion
#region UserRole
static IQueryable<UserRole> SetIncludes(this DbSet<UserRole> roles) =>
roles.Include(x => x.Role);
static IQueryable<UserRole> Search(this IQueryable<UserRole> roles, string search0 +>
roles.Where(x =>
x.Role.Value.ToLower().Contains(search.ToLower())
);
public static async Task<QueryResult<USerRole>> QueryUserRoles(
this AppDbContext db,
int userId,
string page,
string pageSize,
string search,
string sort
)
{
var container = new QueryContainer<UserRole>(
db.UserRoles
.SetIncludes()
.Where(x => x.UserId == userId),
page, pageSize, search, sort
);
return await container.Query((roles, s) =>
roles.SetupSearch(s, (values, term) =>
values.Search(term)
)
);
}
public static async Task<List<string>> GetCurrentRoles(this AppDbContext db, Guid userGuid)
{
var userId = await db.GetUserIdByGuid(userGuid);
if (userId > 0)
{
return await db.UserRoles
.Where(x => x.UserId == userId)
.Select(x => x.Role.Value)
.ToListAsync();
}
else
return new List<string>();
}
public static async Task<List<UserRole>> GetUserRoles(this AppDbContext db, int userId) =>
await db.UserRoles
.SetIncludes()
.Where(x => x.UserId == userId)
.OrderBy(x => x.Role.Value)
.ToListAsync();
public static async Task<List<Role>> GetAvailableRoles(this AppDbContext db, int userId)
{
var ids = await db.UserRoles
.Where(x => x.UserId == userId)
.Select(x => x.RoleId)
.ToListAsync();
return await db.Roles
.Where(x => !ids.Contains(x.Id))
.OrderBy(x => x.Value)
.ToListAsync();
}
public static async Task<User> AddUserRole(this AppDbContext db, UserRole role)
{
if (await role.Validate(db))
{
role.ClearNavProps();
await db.UserRoles.AddAsync(role);
await db.SaveChangesAsync();
return await db.GetUser(role.UserId);
}
return null;
}
public static async Task<User> RemoveUserRole(this AppDbContext db, UserRole role)
{
db.UserRoles.Remove(role);
await db.SaveChangesAsync();
return await db.GetUser(role.UserId);
}
public static async Task<bool> Validate(this UserRole role, AppDbContext db)
{
if (role.UserId < 1)
throw new AppException("Role assignment must include a User", ExceptionType.Validation);
if (role.RoleId < 1)
throw new AppException("Role assignment must include a Role", ExceptionType.Validation);
var check = await db.UserRoles
.FirstOrDefaultAsync(x =>
x.Id != role.Id
&& x.UserId = role.UserId
&& x.RoleId == role.RoleId
);
if (check is not null)
throw new AppException("The provided User is already assigned the specified Role", ExceptionType.Validation);
return true;
}
static void ClearNavProps(this UserRole role)
{
role.User = null;
role.Role = null;
}
#endregion
}
namespace App.Data.Extensions;
using App.Core;
using App.Core.ApiQuery;
using App.Data.Entities;
using App.Core.Extensions;
using Microsoft.EntityFrameworkCore;
public static class NotificationExtensions
{
static IQueryable<Notification> Search(this IQueryable<Notification> notifications, string search) =>
notifications.Where(x =>
x.Message.ToLower().Contains(search.ToLower())
|| x.Url.ToLower().Contains(search.ToLower())
);
public static async Task<QueryResult<Notification>> QueryNotifications(
this AppDbContext db,
int userId,
string page,
string pageSize,
string search,
string sort
)
{
var container = new QueryContainer<Notification>(
db.Notifications
.Where(x => x.UserId == userId),
page, pageSize, search, sort
);
return await container.Query((notifications, s) =>
notifications.SetupSearch(s, (values, term) =>
values.Search(term)
)
);
}
public static async Task<QueryResult<Notification>> QueryUnreadNotifications(
this AppDbContext db,
int userId,
string page,
string pageSize,
string search,
string sort
)
{
var container = new QueryContainer<Notification>(
db.Notifications
.Where(x =>
x.UserId == userId,
&& !x.IsRead
),
page, pageSize, search, sort
);
return await container.Query((notifications, s) =>
notifications.SetupSearch(s, (values, term) =>
values.Search(term)
)
);
}
public static async Task<QueryResult<Notification>> QueryReadNotifications(
this AppDbContext db,
int userId,
string page,
string pageSize,
string search,
string sort
)
{
var container = new QueryContainer<Notification>(
db.Notifications
.Where(x =>
x.UserId == userId
&& x.IsRead
),
page, pageSize, search, sort
);
return await container.Query((notifications, s) =>
notifications.SetupSearch(s, (values, term) =>
values.Search(term)
)
);
}
public static async Task<List<Notification>> GetUnreadNotifications(this AppDbContext db, int userId) =>
await db.Notifications
.Where(x =>
x.UserId == userId
&& !x.IsRead
)
.OrderByDescending(x => x.PushDate)
.ToListAsync();
public static int GetNotificationCount(this AppDbContext db, int userId) =>
db.Notifications
.Where(x =>
x.UserId == userId
&& !x.IsRead
)
.Count();
public static async Task<User> Add(this Notification notification, AppDbContext db)
{
if (notification.Validate())
{
notification.PushDate = DateTime.UtcNow.ToJsDateString();
await db.Notifications.AddAsync(notification);
await db.SaveChangesAsync();
return await db.GetUser(notification.UserId);
}
return null;
}
public static async Task Toggle(this Notification notification, AppDbContext db)
{
notification.IsRead = !notification.IsRead;
db.Notifications.Update(notification);
await db.SaveChangesAsync();
}
public static async Task Remove(this Notification notification, AppDbContext db)
{
db.Notifications.Remove(notification);
await db.SaveChangesAsync();
}
static bool Validate(this Notification notification)
{
if (notification.UserId < 0)
throw new AppException("Notification must target a User", ExceptionType.Validation);
if (string.IsNullOrEmpty(notification.Message))
throw new AppException("Notification must have a Message", ExceptionType.Validation);
return true;
}
}
namespace App.Data.Extensions;
using App.Core;
using App.Core.ApiQuery;
using App.Core.Extensions;
using App.Data.Entities;
public static class OrganizationExtensions
{
static IQueryable<Organization> Search(this IQueryable<Organization> organizations, string search) =>
organizations.Where(x => x.Name.ToLower().Contains(search.ToLower());
public static async Task<string> GetOrganizationImage(this AppDbContext db, int orgId, string url)
{
try
{
var image = await db.GetOrganizationImage(orgId);
if (image is not null)
return image.Url;
return url.GetDefaultImage();
}
catch
{
return url.GetDefaultImage();
}
}
public static async Task<QueryResult<Organization>> QueryOrganizations(
this AppDbContext db,
string page,
string pageSize,
string search,
string sort
)
{
var container = new QueryContainer<Organization>(
db.Organizations,
page, pageSize, search, sort
);
return await container.Query((organizations, s) =>
organizations.SetupSearch(s, (values, term) =>
values.Search(term)
)
);
}
public static async Task<List<Organization>> GetOrganizations(this AppDbContext db) =>
await db.Organizations
.OrderBy(x => x.Name)
.ToListAsync();
public static async Task<Organization> GetOrganization(this AppDbContext db, int id) =>
await db.Organizations
.FindAsync(id);
public static async Task<bool> ValidateName(this Organization org, AppDbContext db) =>
!await db.Organizations
.AnyAsync(x =>
x.Id != org.Id
&& x.Name.ToLower() == org.Name.ToLower()
);
public static async Task<int> Save(this Organization org, AppDbContext db)
{
if (await org.Validate(db))
{
if (org.Id > 0)
await org.Update(db);
else
await org.Add(db);
return org.Id;
}
return 0;
}
public static async Task Remove(this Organization org, AppDbContext db)
{
db.Organizations.Remove(org);
await db.SaveChangesAsync();
}
static async Task Add(this Organization org, AppDbContext db)
{
await db.Organizations.AddAsync(org);
await db.SaveChangesAsync();
}
static async Task Update(this Organization org, AppDbContext db)
{
db.Organizations.Update(org);
await db.SaveChangesAsync();
}
static async Task<bool> Validate(this Organization org, AppDbContext db)
{
if (string.IsNullOrEmpty(org.Name))
throw new AppException("Organization must have a name", ExceptionType.Validation);
var check = await org.ValidateName(db);
if (check is false)
throw new AppException($"{org.Name} is already an Organization", ExceptionType.Validation);
return true;
}
}
namespace App.Data.Extensions;
using App.Core;
using App.Core.ApiQuery;
using App.Core.Extensions;
using App.Data.Entities;
using App.Identity;
public static class ProcessExtensions
{
#region Process
static IQueryable<Process> SetIncludes(this DbSet<Process> processes) =>
processes
.Include(x => x.Role)
.Include(x => x.User);
static IQueryable<Process> Search(this IQueryable<Process> processes, string search) =>
processes.Where(x =>
x.Name.ToLower().Contains(search.ToLower())
|| x.Workflow.Name.ToLower().Contains(search.ToLower())
|| x.User.LastName.ToLower().Contains(search.ToLower())
|| x.User.FirstName.ToLower().Contains(search.ToLower())
|| x.Role.Value.ToLower().Contains(search.ToLower())
);
public static async Task<QueryResult<Process>> QueryProcesses(
this AppDbContext db,
int workflowId,
string page,
string pageSize,
string search,
string sort
)
{
var container = new QueryContainer<Process>(
db.Processes
.SetIncludes()
.Where(x => x.WorkflowId == workflowId),
page, pageSize, search, sort
);
return await container.Query((processes, s) =>
processes.SetupSearch(s, (values, term) =>
values.Search(term)
)
);
}
public static async Task<List<Process>> GetProcesses(this AppDbContext db, int workflowId) =>
await db.Processes
.SetIncludes()
.Where(x => x.WorkflowId == workflowId)
.OrderBy(x => x.Index)
.ToListAsync();
public static async Task<Process> GetProcess(this AppDbContext db, int id) =>
await db.Processes
.SetIncludes()
.FirstOrDefaultAsync(x => x.Id == id);
public static async Task<bool> ValidateName(this Process process, AppDbContext db) =>
!await db.Processes
.AnyAsync(x =>
x.Id != process.Id
&& x.WorkflowId == process.WorkflowId
&& x.Name.ToLower() == process.Name.ToLower()
);
public static async Task<int> Save(this Process process, AppDbContext db)
{
if (await process.Validate(db))
{
if (process.Id > 0)
await process.Update(db);
else
await process.Add(db);
return process.Id;
}
return 0;
}
public static async Task Move(this Process process, AppDbContext db, bool increment)
{
if (await db.IsWorkflowPending(process.WorkflowId))
{
db.Processes.Attach(process);
var siblings = await db.Processes
.Where(x =>
x.Id != process.Id
&& x.WorkflowId == process.WorkflowId
&& !x.IsApproved.HasValue
&& !x.IsRejected.HasValue
).ToListAsync();
if (siblings.Count > 0)
{
if (increment)
{
if (process.Index > siblings.Select(x => x.Index).Max())
{
process.Index = siblings.Select(x => x.Index).Min();
siblings.ForEach(x => x.Index += 1);
db.Processes.UpdateRange(siblings);
}
else
{
var swap = siblings.FirstOrDefaultAsync(x => x.Index == process.Index + 1);
if (swap is not null)
{
swap.Index = process.Index;
process.Index += 1;
db.Processes.Update(swap);
}
}
}
else
{
if (process.Index < siblings.Select(x => x.Index).Min())
{
process.Index = siblings.Select(x => x.Index).Max();
siblings.ForEach(x => x.Index -= 1);
db.Processes.UpdateRange(siblings);
}
else
{
var swap = siblings.FirstOrDefault(p => p.Index == process.Index - 1);
if (swap is not null)
{
swap.Index = process.Index;
process.Index -= 1;
db.Processes.Update(swap);
}
}
}
await db.SaveChangesAsync();
}
}
}
public static async Task Respond(this Process process, AppDbContext db, IUserProvider provider, bool approve, bool? reject)
{
process = await db.Processes
.FindAsync(process.Id);
if (process.IsApproved.HasValue && process.IsApproved.Value == approve)
process.Clear();
else
{
process.UserId = await db.GetUserIdByGuid(provider.CurrentUser.Guid.Value);
process.IsApproved = approve;
process.IsRejected = reject;
process.ActionDate = DateTime.Now;
}
if (process.IsApproved.HasValue && process.IsApproved.Value)
await process.VerifyWorkflowCompletion(db);
await process.Update(db);
}
public static async Task ClearResponse(this Process process, AppDbContext db)
{
process.ClearNavProps();
process.Clear();
await process.Update(db);
}
public static async Task<int> Remove(this Process process, AppDbContext db)
{
var id = process.Id;
await process.NormalizeIndices(db);
db.Processes.Remove(process);
await db.SaveChangeAsync();
return id;
}
static async Task VerifyWorkflowCompletion(this Process process, AppDbContext db)
{
var next = await db.Processes
.FirstOrDefaultAsync(x =>
x.WorkflowId == process.WorkflowId
&& x.Index > process.Index
);
if (next is null)
await db.CompleteWorkflow(process.WorkflowId);
}
static void Clear(this Process process)
{
process.IsApproved = null;
process.IsRejected = null;
process.UserId = null
process.ActionDate = null;
}
static async Task Add(this Process process, AppDbContext db)
{
process.Index = await process.SetIndex(db);
await db.Processes.AddAsync(process);
await db.SaveChangesAsync();
}
static async Task Update(this Process process, AppDbContext db)
{
process.ClearNavProps();
db.Processes.Update(process);
await db.SaveChangesAsync();
}
static async Task<int> SetIndex(this Process process, AppDbContext db)
{
if (await db.Processes
.AnyAsync(x => x.WorkflowId == process.WorkflowId))
{
return await db.Processes
.Where(x => x.WorkflowId == process.WorkflowId)
.MaxAsync(x => x.Index + 1);
}
else
return 0;
}
static async Task NormalizeIndices(this Process process, AppDbContext db)
{
var siblings = await db.Processes
.Where(x =>
x.WorkflowId == process.WorkflowId
&& x.Id != process.Id
&& x.Index > process.Index
)
.ToListAsync();
if (siblings.Count > 0)
{
siblings.ForEach(x => x.Index -= 1);
db.Processes.UpdateRange(siblings);
}
}
static void ClearNavProps(this Process process)
{
process.User = null;
process.Role = null;
process.Workflow = null;
}
static async Task<bool> Validate(this Process process, AppDbContext db)
{
if (string.IsNullOrEmpty(process.Name))
throw new AppException("Process must have a Name", ExceptionType.Validation);
var check = await process.ValidateName(db);
if (check is false)
throw new AppException($"{process.Name} is already a Process", ExceptionType.Validation);
return true;
}
#endregion
#region Process Template
public static async Task<List<ProcessT>> GetProcessTs(this AppDbContext db, int workflowTId) =>
await db.ProcessTs
.Where(x => x.WorkflowTId == workflowTId)
.OrderBy(x => x.Index)
.ToListAsync();
public static async Task<ProcessT> GetProcessT(this AppDbContext db, int id) =>
await db.ProcessTs
.FindAsync(id);
public static async Task<bool> Verify(this ProcessT process, AppDbContext db) =>
!await db.ProcessTs
.AnyAsync(x =>
x.Id != process.Id
&& x.WorkflowTId == process.WorkflowTId
&& x.Name.ToLower() == process.Name.ToLower()
);
public static async Task<ProcessT> Save(this ProcessT process, AppDbContext db)
{
if (await process.Validate(db))
{
if (process.Id > 0)
await process.Update(db);
else
await process.Add(db);
return process;
}
return null;
}
public static async Task Move(this ProcessT process, AppDbContext db, bool increment)
{
db.ProcessTs.Attach(process);
var siblings = await db.ProcessTs
.Where(x =>
x.Id != process.Id
&& x.WorkflowTId == process.WorkflowTId
).ToListAsync();
if (siblings.Count > 0)
{
if (increment)
{
if (process.Index == siblings.Count)
{
process.Index = 0;
siblings.ForEach(x => x.Index += 1);
db.ProcessTs.UpdateRange(siblings);
}
else
{
var swap = siblings.FirstOrDefault(x => x.Index == process.Index + 1);
if (swap is not null)
{
swap.Index = process.Index;
process.Index += 1;
db.ProcessTs.Update(swap);
}
}
}
else
{
if (process.Index == 0)
{
process.Index = siblings.Count;
siblings.ForEach(x => x.Index -= 1);
db.ProcessTs.UpdateRange(siblings);
}
else
{
var swap = siblings.FirstOrDefault(x => x.Index == process.Index - 1);
if (swap is not null)
{
swap.Index = process.Index;
process.Index -= 1;
db.ProcessTs.Update(swap);
}
}
}
await db.SaveChangesAsync();
}
}
public static async Task RemoveProcessT(this AppDbContext db, ProcessT process)
{
await process.NormalizeIndices(db);
db.ProcessTs.Remove(process);
await db.SaveChangesAsync();
}
static async Task Add(this ProcessT process, AppDbContext db)
{
process.Index = await process.SetIndex(db);
await db.ProcessTs.AddAsync(process);
await db.SaveChangesAsync();
}
static async Task Update(this ProcessT process, AppDbContext db)
{
db.ProcessTs.Update(process);
await db.SaveChangesAsync();
}
static async Task<int> SetIndex(this ProcessT process, AppDbContext db)
{
if (await db.ProcessTs
.AnyAsync(x => x.WorkflowTId == process.WorkflowTId))
{
return await db.ProcessTs
.Where(x => x.WorkflowTId == process.WorkflowTId)
.MaxAsync(x => x.Index + 1);
}
else
return 0;
}
static async Task NormalizeIndices(this ProcessT process, AppDbContext db)
{
var siblings = await db.ProcessTs
.Where(x =>
x.WorkflowTId == process.WorkflowTId
&& x.Id != process.Id
&& x.Index > process.Index
)
.ToListAsync();
if (siblings.Count > 0)
{
siblings.ForEach(x => x.Index -= 1);
db.ProcessTs.UpdateRange(siblings);
}
}
static async Task<bool> Validate(this ProcessT process, AppDbContext db)
{
if (string.IsNullOrEmpty(process.Name))
throw new AppException("Process Template must have a Name", ExceptionType.Validation);
var check = await process.Verify(db);
if (check is null)
throw new AppException($"{process.Name} is already a Process Template", ExceptionType.Validation);
return true;
}
#endregion
#region ProcessReview
static IQueryable<ProcessReview> SetIncludes(this DbSet<ProcessReview> reviews) =>
reviews
.Include(x => x.User)
.Include(x => x.Role);
#endregion
}
namespace App.Data.Models;
public class ProcessAuth
{
public bool Contributor { get; set; }
publci bool Approver { get; set; }
publci ICollection<int> RoleIds { get; set; }
}
namespace App.Data.Models;
using App.Data.Entities;
public class ProcessState
{
public Process Process { get; set; }
public ProcessAuth Permissions { get; set; }
public bool IsCurrent { get; set; }
public bool IsReviewed { get; set; }
public ICollection<ProcessReview> Reviews { get; set; }
}
namespace App.Data.Models;
public class ProcessSync : Sync
{
public bool UpdateNotes { get; set; }
public bool UpdateProcess { get; set; }
public bool UpdateReviews { get; set; }
public bool UpdateUploads { get; set; }
public bool UpdateWorkflow { get; set; }
}
namespace App.Data.Models;
public class Sync
{
public int Id { get; set; }
public bool IsOrigin { get; set; }
public bool IsRemoved { get; set; }
public string Type { get; set; }
}
namespace App.Data.Models;
using App.Data.Entities;
public class WorkflowState
{
public Workflow Workflow { get; set; }
public Process Current { get; set; }
public bool IsPending { get; set; }
public bool IsApproved { get; set; }
public bool IsRejected { get; set; }
public bool IsReturned { get; set; }
public ICollection<WorkflowItem> Items { get; set; }
public ICollection<Process> Processes { get; set; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment