Last active
March 18, 2022 18:23
-
-
Save JaimeStill/e722643f15bd3f0ed30168c4cfd0c287 to your computer and use it in GitHub Desktop.
.NET + Angular distributed reactive architecture + stack updates
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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()); | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace App.Data.Models; | |
public class ProcessAuth | |
{ | |
public bool Contributor { get; set; } | |
publci bool Approver { get; set; } | |
publci ICollection<int> RoleIds { get; set; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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