init
This commit is contained in:
commit
77a536ee79
59
AutoScan.cs
Normal file
59
AutoScan.cs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
namespace NejCommon;
|
||||
|
||||
public interface IScopedService
|
||||
{
|
||||
|
||||
}
|
||||
public interface ISingletonService
|
||||
{
|
||||
|
||||
}
|
||||
public interface ITransientService
|
||||
{
|
||||
|
||||
}
|
||||
public interface IBackgroundService
|
||||
{
|
||||
|
||||
}
|
||||
public interface ISettings
|
||||
{
|
||||
string Path { get; }
|
||||
}
|
||||
|
||||
public static class AutoScan
|
||||
{
|
||||
public static void RegisterServices(this IServiceCollection collection)
|
||||
{
|
||||
collection.Scan(scan => scan
|
||||
// We start out with all types in the assembly of ITransientService
|
||||
.FromAssemblyOf<ITransientService>()
|
||||
// AddClasses starts out with all public, non-abstract types in this assembly.
|
||||
// These types are then filtered by the delegate passed to the method.
|
||||
// In this case, we filter out only the classes that are assignable to ITransientService.
|
||||
.AddClasses(classes => classes.AssignableTo<ITransientService>())
|
||||
// We then specify what type we want to register these classes as.
|
||||
// In this case, we want to register the types as all of its implemented interfaces.
|
||||
// So if a type implements 3 interfaces; A, B, C, we'd end up with three separate registrations.
|
||||
.AsSelf()
|
||||
// And lastly, we specify the lifetime of these registrations.
|
||||
.WithTransientLifetime()
|
||||
// Here we start again, with a new full set of classes from the assembly above.
|
||||
// This time, filtering out only the classes assignable to IScopedService.
|
||||
.AddClasses(classes => classes.AssignableTo<IScopedService>())
|
||||
// Now, we just want to register these types as a single interface, IScopedService.
|
||||
.AsSelf()
|
||||
// And again, just specify the lifetime.
|
||||
.WithScopedLifetime()
|
||||
.AddClasses(classes => classes.AssignableTo<ISingletonService>())
|
||||
.AsSelf()
|
||||
.AsImplementedInterfaces()
|
||||
.WithSingletonLifetime()
|
||||
.AddClasses(classes => classes.AssignableTo<IBackgroundService>())
|
||||
.AsSelf()
|
||||
.AsImplementedInterfaces()
|
||||
.WithSingletonLifetime()
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
172
Controllers/AutoController.cs
Normal file
172
Controllers/AutoController.cs
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using ApiSourceGeneratorHelper;
|
||||
using AutoMapPropertyHelper;
|
||||
using EntityFrameworkCore.Projectables;
|
||||
using EntityFrameworkCore.Projectables.Extensions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NejAccountingAPI.Controllers;
|
||||
using NejAccountingAPI.Models;
|
||||
using NejCommon.Utils;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
|
||||
namespace NejCommon.Controllers
|
||||
{
|
||||
public abstract class AutoController<TType, TRequest, TResponse> : AutoController<Company, TType, TRequest, TResponse> where TType : class, new() where TRequest : IAutomappedAttribute<TType, TRequest>, new() where TResponse : IAutomappedAttribute<TType, TResponse>, new()
|
||||
{
|
||||
public AutoController(AppDbContext appDb, IServiceProvider providers) : base(appDb, providers)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class AutoController<TOwner, TType, TRequest, TResponse> : AutoController<TOwner, TType, TResponse, TRequest, TResponse, TResponse, TRequest, TResponse>
|
||||
where TType : class, new()
|
||||
where TRequest : IAutomappedAttribute<TType, TRequest>, new()
|
||||
where TResponse : IAutomappedAttribute<TType, TResponse>, new()
|
||||
{
|
||||
public AutoController(AppDbContext appDb, IServiceProvider providers) : base(appDb, providers)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <typeparam name="TType">The underyling type</typeparam>
|
||||
/// <typeparam name="TRequest">The request type</typeparam>
|
||||
/// <typeparam name="TResponse">The response type</typeparam>
|
||||
[ApiController]
|
||||
public abstract class AutoController<TOwner, TType, TGetAllResponse, TCreateRequest, TCreateResponse, TGetResponse, TUpdateRequest, TUpdateResponse> : ControllerBase
|
||||
where TType : class, new()
|
||||
where TGetAllResponse : IAutomappedAttribute<TType, TGetAllResponse>, new()
|
||||
where TCreateRequest : IAutomappedAttribute<TType, TCreateRequest>, new()
|
||||
where TCreateResponse : IAutomappedAttribute<TType, TCreateResponse>, new()
|
||||
where TGetResponse : IAutomappedAttribute<TType, TGetResponse>, new()
|
||||
where TUpdateRequest : IAutomappedAttribute<TType, TUpdateRequest>, new()
|
||||
where TUpdateResponse : IAutomappedAttribute<TType, TUpdateResponse>, new()
|
||||
{
|
||||
protected readonly AppDbContext db;
|
||||
protected readonly IServiceProvider providers;
|
||||
|
||||
public AutoController(AppDbContext appDb, IServiceProvider providers) : base()
|
||||
{
|
||||
db = appDb;
|
||||
this.providers = providers;
|
||||
}
|
||||
|
||||
protected abstract IQueryable<TType> GetQuery(TOwner comp);
|
||||
protected virtual TType AssociateWithParent(TType entity, TOwner comp)
|
||||
{
|
||||
if (typeof(TOwner) != typeof(Company))
|
||||
{
|
||||
throw new NotImplementedException("Special parent association not implemented");
|
||||
}
|
||||
|
||||
var props = typeof(TType).GetProperty("Company");
|
||||
if (props == null)
|
||||
{
|
||||
//not implemented
|
||||
throw new NotImplementedException("Special parent association not implemented");
|
||||
}
|
||||
props.SetValue(entity, comp);
|
||||
return entity;
|
||||
}
|
||||
protected virtual IQueryable<TType> ApplyDefaultOrdering(IQueryable<TType> query)
|
||||
{
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get all the <typeparamref name="TType"/>
|
||||
/// </summary>
|
||||
/// <response code="200">Success</response>
|
||||
/// <response code="0">There was an error</response>
|
||||
[HttpGet]
|
||||
public virtual async Task<Results<BadRequest<Error>, Ok<PaginationResponse<TGetAllResponse>>>> GetAll(TOwner company, [FromQuery] Pagination pag)
|
||||
{
|
||||
var data = await ApplyDefaultOrdering(GetQuery(company)).AsNoTrackingWithIdentityResolution().ApplyPaginationRes<TType, TGetAllResponse>(providers, pag);
|
||||
|
||||
//Console.Writeline(data.Data);
|
||||
|
||||
return TypedResults.Ok(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <typeparamref name="TType"/>
|
||||
/// </summary>
|
||||
/// <param name="body"></param>
|
||||
/// <response code="201">Success</response>
|
||||
/// <response code="0">There was an error</response>
|
||||
[HttpPost]
|
||||
public virtual async Task<Results<BadRequest<Error>, CreatedAtRoute<TCreateResponse>>> Create(TOwner company, [FromBody] TCreateRequest body)
|
||||
{
|
||||
var entity = db.Create<TType>();
|
||||
|
||||
entity = AssociateWithParent(entity, company);
|
||||
|
||||
await db.AddAsync(entity);
|
||||
|
||||
body.ApplyTo(providers, entity);
|
||||
|
||||
return await db.ApiSaveChangesAsyncCreate<TType, TCreateResponse>(providers, entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <typeparamref name="TType"/>
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// <response code="200">Success</response>
|
||||
/// <response code="0">There was an error</response>
|
||||
[HttpGet]
|
||||
[Route("{id}/")]
|
||||
public virtual async Task<Results<NotFound, Ok<TGetResponse>>> Get([FromServices][ModelBinder(Name = "id")] TType entity)
|
||||
{
|
||||
var dat = new TGetResponse().ApplyFrom(providers, entity);
|
||||
return TypedResults.Ok(dat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete company.
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// <response code="200">Success</response>
|
||||
/// <response code="0">There was an error</response>
|
||||
[HttpDelete]
|
||||
[Route("{id}/")]
|
||||
public virtual async Task<Results<BadRequest<Error>, Ok>> Delete([FromServices][ModelBinder(Name = "id")] TType entity)
|
||||
{
|
||||
db.Remove(entity!);
|
||||
|
||||
return await db.ApiSaveChangesAsync(TypedResults.Ok());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update company.
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// <param name="body"></param>
|
||||
/// <response code="200">Success</response>
|
||||
/// <response code="0">There was an error</response>
|
||||
[HttpPut]
|
||||
[Route("{id}/")]
|
||||
public virtual async Task<Results<NotFound, BadRequest<Error>, Ok<TUpdateResponse>>> Update([FromServices][ModelBinder(Name = "id")] TType entity, [FromBody] TUpdateRequest body)
|
||||
{/*
|
||||
if(entity is InvoiceBase inv){
|
||||
//Console.Writeline(inv.InvoiceItems.Count);
|
||||
}*/
|
||||
body.ApplyTo(providers, entity);
|
||||
|
||||
var dat = new TUpdateResponse().ApplyFrom(providers, entity);
|
||||
|
||||
var res = await db.ApiSaveChangesAsyncOk<TType, TUpdateResponse>(providers, entity);
|
||||
|
||||
//use the private constructor thru reflection
|
||||
var ctor = typeof(Results<NotFound, BadRequest<Error>, Ok<TUpdateResponse>>).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)[0];
|
||||
return (Results<NotFound, BadRequest<Error>, Ok<TUpdateResponse>>)ctor.Invoke(new object[] { res.Result });
|
||||
}
|
||||
}
|
||||
}
|
||||
118
Controllers/IApiDescriptionVisibilitySchemaFilter.cs
Normal file
118
Controllers/IApiDescriptionVisibilitySchemaFilter.cs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace NejCommon.Controllers
|
||||
{
|
||||
public class IApiDescriptionVisibilitySchemaFilter : ISchemaFilter
|
||||
{
|
||||
List<OpenApiSchema> schemas = new List<OpenApiSchema>();
|
||||
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
|
||||
{
|
||||
RemoveSchemas(context);
|
||||
//Get the .net type of the schema
|
||||
var type = context.Type;
|
||||
|
||||
//Get attributes that imlement IApiDescriptionVisibilityProvider interface
|
||||
var attributes = type.GetCustomAttributes(typeof(IApiDescriptionVisibilityProvider), true).Select(a => a as IApiDescriptionVisibilityProvider);
|
||||
|
||||
//If there are no attributes, then return
|
||||
if (!attributes.Any())
|
||||
return;
|
||||
|
||||
var ignore = attributes.Any(a => a.IgnoreApi);
|
||||
|
||||
//If the schema is ignored, then remove it from the schema repository
|
||||
if (!ignore)
|
||||
return;
|
||||
|
||||
schemas.Add(schema);
|
||||
RemoveSchemas(context);
|
||||
}
|
||||
|
||||
public void RemoveSchemas(SchemaFilterContext context)
|
||||
{
|
||||
foreach (var schema in schemas)
|
||||
{
|
||||
var key = context.SchemaRepository.Schemas.FirstOrDefault(k => k.Value == schema).Key;
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
continue;
|
||||
//Console.WriteLine($"Removing schema {key}");
|
||||
context.SchemaRepository.Schemas.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class OrphanedFilter : IDocumentFilter
|
||||
{
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
// Add all paths parameters, bodies, responses and query parameters to a list
|
||||
var ops = swaggerDoc.Paths.SelectMany(p => p.Value.Operations).ToList();
|
||||
|
||||
var Parameters = ops
|
||||
.SelectMany(o => o.Value.Parameters)
|
||||
.Select(x => x.Schema).ToList();
|
||||
var requst = ops
|
||||
.Select(o => o.Value.RequestBody)
|
||||
.Where(c => c != null)
|
||||
.SelectMany(x => x.Content)
|
||||
.Select(c => c.Value.Schema).ToList();
|
||||
var res = ops
|
||||
.SelectMany(o => o.Value.Responses)
|
||||
.SelectMany(r => r.Value.Content)
|
||||
.Select(c => c.Value.Schema).ToList();
|
||||
|
||||
var usedSchemas = Parameters.Union(requst).Union(res).Distinct();
|
||||
|
||||
/*
|
||||
Console.WriteLine($"Used schemas: {string.Join(", ", swaggerDoc.Components.Parameters)}");
|
||||
|
||||
var usedSchemas = swaggerDoc.Components.Parameters.Select(x => x.Value.Schema).
|
||||
Union(swaggerDoc.Components.RequestBodies.Select(x => x.Value.Content).SelectMany(x => x.Values).Select(x => x.Schema)).
|
||||
Union(swaggerDoc.Components.Responses.Select(x => x.Value.Content).SelectMany(x => x.Values).Select(x => x.Schema)).
|
||||
Distinct();
|
||||
*/
|
||||
var schemas = usedSchemas.ToList();
|
||||
|
||||
var currentSchemas = new Dictionary<string, OpenApiSchema>();
|
||||
foreach(var sch in schemas){
|
||||
ExpandSchemas(context.SchemaRepository, currentSchemas, sch);
|
||||
}
|
||||
|
||||
//Console.WriteLine($"Used schemas: {string.Join(", ", currentSchemas.Keys)}");
|
||||
|
||||
var orphanedSchemas = swaggerDoc.Components.Schemas.Where(x => !currentSchemas.ContainsKey(x.Key)).ToDictionary(x => x.Key, x => x.Value);
|
||||
|
||||
//Console.WriteLine($"Unused schemas: {string.Join(", ", orphanedSchemas.Keys)}");
|
||||
|
||||
swaggerDoc.Components.Schemas = swaggerDoc.Components.Schemas.Where(x => currentSchemas.ContainsKey(x.Key)).ToDictionary(x => x.Key, x => x.Value);
|
||||
}
|
||||
|
||||
public static void ExpandSchemas(SchemaRepository repo, Dictionary<string, OpenApiSchema> currentSchemas, OpenApiSchema schemaToExpand)
|
||||
{
|
||||
if(schemaToExpand == null)
|
||||
return;
|
||||
|
||||
if(schemaToExpand.Type == "array")
|
||||
schemaToExpand = schemaToExpand.Items;
|
||||
|
||||
var selfRef = schemaToExpand.Reference?.Id;
|
||||
var properties = schemaToExpand.Properties.Values.ToList();
|
||||
|
||||
if(selfRef != null && !currentSchemas.ContainsKey(selfRef) && repo.Schemas.ContainsKey(selfRef))
|
||||
{
|
||||
var sch = repo.Schemas[selfRef];
|
||||
currentSchemas.Add(selfRef, sch);
|
||||
|
||||
ExpandSchemas(repo, currentSchemas, sch);
|
||||
}
|
||||
foreach(var sch in properties)
|
||||
{
|
||||
ExpandSchemas(repo, currentSchemas, sch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
62
Controllers/ResponseEnricher.cs
Normal file
62
Controllers/ResponseEnricher.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Serialization;
|
||||
using ClosedXML.Excel;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace NejCommon.Controllers;
|
||||
|
||||
public static class Responses
|
||||
{
|
||||
public static FileStreamHttpResult RespondXlsx(this ControllerBase controller, XLWorkbook file, string fileName)
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
file.SaveAs(stream);
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
controller.Response.Headers.Add("Content-Disposition", "inline; filename=" + fileName);
|
||||
return TypedResults.File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName);
|
||||
}
|
||||
public static FileStreamHttpResult RespondXlsxTable<T>(this ControllerBase controller, IEnumerable<T> data, string name = "Data", string? sheetName = null)
|
||||
{
|
||||
if (sheetName is null)
|
||||
{
|
||||
//limit the name to 31 characters
|
||||
sheetName = name.Length > 31 ? name.Substring(0, 31) : name;
|
||||
}
|
||||
|
||||
var notebook = new XLWorkbook();
|
||||
var sheet = notebook.Worksheets.Add(sheetName);
|
||||
sheet.FirstCell().InsertTable(data);
|
||||
|
||||
sheet.Columns().AdjustToContents();
|
||||
|
||||
return controller.RespondXlsx(notebook, name + ".xlsx");
|
||||
}
|
||||
|
||||
public static FileStreamHttpResult RespondXml<T>(this ControllerBase controller, T obj, string fileName = "Data.xml")
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
//use utf8 encoding
|
||||
var serializer = new XmlSerializer(typeof(T));
|
||||
var settings = new XmlWriterSettings
|
||||
{
|
||||
Indent = true,
|
||||
Encoding = Encoding.UTF8,
|
||||
};
|
||||
|
||||
//Console.Writeline(stream.Length);
|
||||
|
||||
using (var writer = XmlWriter.Create(stream, settings))
|
||||
{
|
||||
serializer.Serialize(writer, obj);
|
||||
//Console.Writeline(stream.Length);
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
controller.Response.Headers.Add("Content-Disposition", "inline; filename=" + fileName);
|
||||
return TypedResults.File(stream, "text/xml", fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
108
Controllers/TypedResultsPolyfill.cs
Normal file
108
Controllers/TypedResultsPolyfill.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
|
||||
using System.Diagnostics.Eventing.Reader;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace NejCommon.Controllers
|
||||
{
|
||||
public class TypedResultsMetadataProvider : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
var responseType = context.MethodInfo.ReturnType;
|
||||
//Console.WriteLine(context.MethodInfo.DeclaringType.Name);
|
||||
//Console.WriteLine(context.MethodInfo.Name);
|
||||
//Console.WriteLine(responseType);
|
||||
var t = IsSubclassOfRawGeneric(typeof(Microsoft.AspNetCore.Http.HttpResults.Results<,>), responseType);
|
||||
if (t == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var parArg = t.GetGenericArguments();
|
||||
if (operation.Responses.ContainsKey("200"))
|
||||
operation.Responses.Remove("200");
|
||||
|
||||
foreach (var arg in parArg)
|
||||
{
|
||||
if (arg == typeof(NotFound))
|
||||
{
|
||||
operation.Responses.Add("404", new OpenApiResponse { Description = "Not found" });
|
||||
}
|
||||
else if (arg == typeof(Ok))
|
||||
{
|
||||
operation.Responses.Add("200", new OpenApiResponse { Description = "Success" });
|
||||
}
|
||||
else if (IsSubclassOfRawGeneric(typeof(Ok<>), arg) != null)
|
||||
{
|
||||
|
||||
var okArg = IsSubclassOfRawGeneric(typeof(Ok<>), arg).GetGenericArguments()[0];
|
||||
Console.WriteLine("Adding: " + okArg);
|
||||
|
||||
//get or generate the schema
|
||||
var schema = context.SchemaGenerator.GenerateSchema(okArg, context.SchemaRepository);
|
||||
operation.Responses.Add("200", new OpenApiResponse { Description = "Success", Content = { { "application/json", new OpenApiMediaType { Schema = schema } } } });
|
||||
|
||||
}
|
||||
else if (arg == typeof(CreatedAtRoute))
|
||||
{
|
||||
operation.Responses.Add("201", new OpenApiResponse { Description = "Success" });
|
||||
}
|
||||
else if (IsSubclassOfRawGeneric(typeof(CreatedAtRoute<>), arg) != null)
|
||||
{
|
||||
if (operation.Responses.ContainsKey("201"))
|
||||
operation.Responses.Remove("201");
|
||||
|
||||
var okArg = IsSubclassOfRawGeneric(typeof(CreatedAtRoute<>), arg).GetGenericArguments()[0];
|
||||
Console.WriteLine("Adding: " + okArg);
|
||||
|
||||
//get or generate the schema
|
||||
var schema = context.SchemaGenerator.GenerateSchema(okArg, context.SchemaRepository);
|
||||
operation.Responses.Add("201", new OpenApiResponse { Description = "Success", Content = { { "application/json", new OpenApiMediaType { Schema = schema } } } });
|
||||
}
|
||||
else if (arg == typeof(BadRequest))
|
||||
{
|
||||
operation.Responses.Add("400", new OpenApiResponse { Description = "There was an error" });
|
||||
}
|
||||
else if (IsSubclassOfRawGeneric(typeof(BadRequest<>), arg) != null)
|
||||
{
|
||||
if (operation.Responses.ContainsKey("400"))
|
||||
operation.Responses.Remove("400");
|
||||
|
||||
var okArg = IsSubclassOfRawGeneric(typeof(BadRequest<>), arg).GetGenericArguments()[0];
|
||||
Console.WriteLine("Adding: " + okArg);
|
||||
|
||||
//get or generate the schema
|
||||
var schema = context.SchemaGenerator.GenerateSchema(okArg, context.SchemaRepository);
|
||||
operation.Responses.Add("400", new OpenApiResponse { Description = "There was an error", Content = { { "application/json", new OpenApiMediaType { Schema = schema } } } });
|
||||
}
|
||||
else if (arg == typeof(FileStreamHttpResult)){
|
||||
operation.Responses.Add("200", new OpenApiResponse { Description = "Success", Content = { { "application/octet-stream", new OpenApiMediaType { Schema = new OpenApiSchema { Type = "string", Format = "binary" } } } } });
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Unknown type: " + arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Type? IsSubclassOfRawGeneric(Type generic, Type toCheck)
|
||||
{
|
||||
while (toCheck != null && toCheck != typeof(object))
|
||||
{
|
||||
//if Task is used, we need to check the underlying type
|
||||
var realTypeNoTask = toCheck.IsGenericType && toCheck.GetGenericTypeDefinition() == typeof(Task<>) ? toCheck.GetGenericArguments()[0] : toCheck;
|
||||
var cur = realTypeNoTask.IsGenericType ? realTypeNoTask.GetGenericTypeDefinition() : realTypeNoTask;
|
||||
//Console.WriteLine(cur);
|
||||
if (generic == cur)
|
||||
{
|
||||
return realTypeNoTask;
|
||||
}
|
||||
toCheck = toCheck.BaseType;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Emails/EmailBase.cs
Normal file
38
Emails/EmailBase.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using BlazorTemplater;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NejAccountingAPI.Services.Email;
|
||||
using PuppeteerSharp;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace NejAccountingAPI.Emails
|
||||
{
|
||||
public abstract class EmailBase<T1, T2> : ComponentBase where T1 : EmailBase<T1, T2>
|
||||
{
|
||||
public static string _css = "";
|
||||
public static string GetCss()
|
||||
{
|
||||
if (_css == "")
|
||||
_css = File.ReadAllText("./NejCommon/Emails/wwwroot/output.css");
|
||||
|
||||
return _css;
|
||||
}
|
||||
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public T2 Data { get; set; } = default!;
|
||||
|
||||
public static async Task<string> GetHTML(T2 dat)
|
||||
{
|
||||
string html = new ComponentRenderer<T1>().Set(x => x.Data, dat).Render();
|
||||
var result = PreMailer.Net.PreMailer.MoveCssInline(html, css: GetCss());
|
||||
|
||||
return result.Html;
|
||||
}
|
||||
|
||||
public virtual string GetSubject()
|
||||
{
|
||||
return "DEV: " + this.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Emails/GeneralMessage.razor
Normal file
23
Emails/GeneralMessage.razor
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
@namespace NejAccountingAPI.Emails
|
||||
|
||||
@inherits EmailBase<GeneralMessage, GeneralMessage.GeneralMessageData>
|
||||
|
||||
@code {
|
||||
|
||||
public struct GeneralMessageData
|
||||
{
|
||||
public GeneralMessageData(string subject, string message)
|
||||
{
|
||||
Message = message;
|
||||
Subject = subject;
|
||||
}
|
||||
public string Message;
|
||||
public string Subject;
|
||||
}
|
||||
|
||||
public override string GetSubject()
|
||||
{
|
||||
return base.GetSubject();
|
||||
}
|
||||
}
|
||||
|
||||
1
Emails/__Imports.razor
Normal file
1
Emails/__Imports.razor
Normal file
|
|
@ -0,0 +1 @@
|
|||
@namespace NejAccountingAPI.Emails
|
||||
828
Emails/wwwroot/output.css
Normal file
828
Emails/wwwroot/output.css
Normal file
|
|
@ -0,0 +1,828 @@
|
|||
/*
|
||||
! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
border-width: 0;
|
||||
/* 2 */
|
||||
border-style: solid;
|
||||
/* 2 */
|
||||
border-color: #e5e7eb;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
::before,
|
||||
::after {
|
||||
--tw-content: '';
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
/* 1 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 2 */
|
||||
-moz-tab-size: 4;
|
||||
/* 3 */
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
/* 3 */
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
/* 4 */
|
||||
font-feature-settings: normal;
|
||||
/* 5 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove the margin in all browsers.
|
||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Ensure horizontal rules are visible by default.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 2 */
|
||||
border-top-width: 1px;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use the user's configured `mono` font family by default.
|
||||
2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0;
|
||||
/* 1 */
|
||||
border-color: inherit;
|
||||
/* 2 */
|
||||
border-collapse: collapse;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
3. Remove default padding in all browsers.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
/* 1 */
|
||||
font-size: 100%;
|
||||
/* 1 */
|
||||
font-weight: inherit;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
/* 2 */
|
||||
padding: 0;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inheritance of text transform in Edge and Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Remove default button styles.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
background-color: transparent;
|
||||
/* 2 */
|
||||
background-image: none;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||
*/
|
||||
|
||||
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Set the default cursor for buttons.
|
||||
*/
|
||||
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure disabled buttons don't get the pointer cursor.
|
||||
*/
|
||||
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block;
|
||||
/* 1 */
|
||||
vertical-align: middle;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
*, ::before, ::after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
::backdrop {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
max-width: 640px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
max-width: 1024px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.container {
|
||||
max-width: 1536px;
|
||||
}
|
||||
}
|
||||
|
||||
.static {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.table {
|
||||
display: table;
|
||||
}
|
||||
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.h-16 {
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
.h-48 {
|
||||
height: 12rem;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-16 {
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.w-48 {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.items-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.self-end {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.border-4 {
|
||||
border-width: 4px;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(var(--primary) / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.py-4 {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.px-10 {
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.pt-2 {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.pb-2 {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.pl-8 {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.pr-8 {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.pr-4 {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.pt-1 {
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
|
||||
.text-xl {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(var(--secondary-text) / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(var(--primary-text) / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.filter {
|
||||
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||
}
|
||||
|
||||
:root {
|
||||
/*
|
||||
--primary: #3d3d3d;
|
||||
--secondary: #535353;
|
||||
--trinary: #2c2c2c;
|
||||
--primary-text: #ffffff;
|
||||
--secondary-text: #a0a0a0;
|
||||
--primary-invert: #f3f3f3;
|
||||
--secondary-invert: #dbdbdb;
|
||||
--trinary-invert: #ffffff;
|
||||
--primary-invert-text: #3d3d3d;
|
||||
--secondary-invert-text: #535353;
|
||||
--accent: #00C800;
|
||||
--accent-dark: #008f00;
|
||||
--accent-light: #00ed00;
|
||||
--accent2: #3080FF;
|
||||
--accent2-dark: #225ab4;
|
||||
--accent3: #804000;
|
||||
--accent3-dark: #472400;
|
||||
--accent4: #F8B02C;
|
||||
--accent4-dark: #bd8724;
|
||||
--accent5: #9E3086;
|
||||
--accent5-dark: #6b215b;
|
||||
*/
|
||||
--primary: 61 61 61;
|
||||
--secondary: 83 83 83;
|
||||
--trinary: 44 44 44;
|
||||
--primary-text: 255 255 255;
|
||||
--secondary-text: 237 246 255;
|
||||
--primary-invert: 243 243 243;
|
||||
--secondary-invert: 219 219 219;
|
||||
--trinary-invert: 255 255 255;
|
||||
--primary-invert-text: 61 61 61;
|
||||
--secondary-invert-text: 83 83 83;
|
||||
--accent: 57 172 231;
|
||||
--accent-dark: 7 132 181;
|
||||
--accent-light: 153 204 255;
|
||||
--accent2: 48 128 255;
|
||||
--accent2-dark: 34 90 180;
|
||||
--accent3: 128 64 0;
|
||||
--accent3-dark: 71 36 0;
|
||||
--accent4: 248 176 44;
|
||||
--accent4-dark: 189 135 36;
|
||||
--accent5: 158 48 134;
|
||||
--accent5-dark: 107 33 91;
|
||||
--primary-border: 61 61 61;
|
||||
--secondary-border: 83 83 83;
|
||||
--trinary-border: 44 44 44;
|
||||
}
|
||||
|
||||
.light\-theme {
|
||||
--primary: 243 243 243;
|
||||
--secondary: 219 219 219;
|
||||
--trinary: 255 255 255;
|
||||
--primary-text: 61 61 61;
|
||||
--secondary-text: 83 83 83;
|
||||
--primary-invert: 61 61 61;
|
||||
--secondary-invert: 83 83 83;
|
||||
--trinary-invert: 44 44 44;
|
||||
--primary-text-invert: 255 255 255;
|
||||
--secondary-text-invert: 160 160 160;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-tap-highlight-color: var(--accent);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--secondary);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-dark);
|
||||
}
|
||||
87
Emails/wwwroot/site.css
Normal file
87
Emails/wwwroot/site.css
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
:root {
|
||||
/*
|
||||
--primary: #3d3d3d;
|
||||
--secondary: #535353;
|
||||
--trinary: #2c2c2c;
|
||||
--primary-text: #ffffff;
|
||||
--secondary-text: #a0a0a0;
|
||||
--primary-invert: #f3f3f3;
|
||||
--secondary-invert: #dbdbdb;
|
||||
--trinary-invert: #ffffff;
|
||||
--primary-invert-text: #3d3d3d;
|
||||
--secondary-invert-text: #535353;
|
||||
--accent: #00C800;
|
||||
--accent-dark: #008f00;
|
||||
--accent-light: #00ed00;
|
||||
--accent2: #3080FF;
|
||||
--accent2-dark: #225ab4;
|
||||
--accent3: #804000;
|
||||
--accent3-dark: #472400;
|
||||
--accent4: #F8B02C;
|
||||
--accent4-dark: #bd8724;
|
||||
--accent5: #9E3086;
|
||||
--accent5-dark: #6b215b;
|
||||
*/
|
||||
--primary: 61 61 61;
|
||||
--secondary: 83 83 83;
|
||||
--trinary: 44 44 44;
|
||||
--primary-text: 255 255 255;
|
||||
--secondary-text: 237 246 255;
|
||||
--primary-invert: 243 243 243;
|
||||
--secondary-invert: 219 219 219;
|
||||
--trinary-invert: 255 255 255;
|
||||
--primary-invert-text: 61 61 61;
|
||||
--secondary-invert-text: 83 83 83;
|
||||
--accent: 57 172 231;
|
||||
--accent-dark: 7 132 181;
|
||||
--accent-light: 153 204 255;
|
||||
--accent2: 48 128 255;
|
||||
--accent2-dark: 34 90 180;
|
||||
--accent3: 128 64 0;
|
||||
--accent3-dark: 71 36 0;
|
||||
--accent4: 248 176 44;
|
||||
--accent4-dark: 189 135 36;
|
||||
--accent5: 158 48 134;
|
||||
--accent5-dark: 107 33 91;
|
||||
--primary-border: 61 61 61;
|
||||
--secondary-border: 83 83 83;
|
||||
--trinary-border: 44 44 44;
|
||||
}
|
||||
|
||||
.light\-theme {
|
||||
--primary: 243 243 243;
|
||||
--secondary: 219 219 219;
|
||||
--trinary: 255 255 255;
|
||||
--primary-text: 61 61 61;
|
||||
--secondary-text: 83 83 83;
|
||||
--primary-invert: 61 61 61;
|
||||
--secondary-invert: 83 83 83;
|
||||
--trinary-invert: 44 44 44;
|
||||
--primary-text-invert: 255 255 255;
|
||||
--secondary-text-invert: 160 160 160;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-tap-highlight-color: var(--accent);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--secondary);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-dark);
|
||||
}
|
||||
1067
Models/Country.cs
Normal file
1067
Models/Country.cs
Normal file
File diff suppressed because it is too large
Load Diff
631
Models/Currency.cs
Normal file
631
Models/Currency.cs
Normal file
|
|
@ -0,0 +1,631 @@
|
|||
|
||||
|
||||
namespace NejCommon.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or Sets Currency
|
||||
/// </summary>
|
||||
public enum Currency
|
||||
{
|
||||
/// <summary>
|
||||
/// AED
|
||||
/// </summary>
|
||||
AED,
|
||||
/// <summary>
|
||||
/// AFN
|
||||
/// </summary>
|
||||
AFN,
|
||||
/// <summary>
|
||||
/// ALL
|
||||
/// </summary>
|
||||
ALL,
|
||||
/// <summary>
|
||||
/// AMD
|
||||
/// </summary>
|
||||
AMD,
|
||||
/// <summary>
|
||||
/// ANG
|
||||
/// </summary>
|
||||
ANG,
|
||||
/// <summary>
|
||||
/// AOA
|
||||
/// </summary>
|
||||
AOA,
|
||||
/// <summary>
|
||||
/// ARS
|
||||
/// </summary>
|
||||
ARS,
|
||||
/// <summary>
|
||||
/// AUD
|
||||
/// </summary>
|
||||
AUD,
|
||||
/// <summary>
|
||||
/// AWG
|
||||
/// </summary>
|
||||
AWG,
|
||||
/// <summary>
|
||||
/// AZN
|
||||
/// </summary>
|
||||
AZN,
|
||||
/// <summary>
|
||||
/// BAM
|
||||
/// </summary>
|
||||
BAM,
|
||||
/// <summary>
|
||||
/// BBD
|
||||
/// </summary>
|
||||
BBD,
|
||||
/// <summary>
|
||||
/// BDT
|
||||
/// </summary>
|
||||
BDT,
|
||||
/// <summary>
|
||||
/// BGN
|
||||
/// </summary>
|
||||
BGN,
|
||||
/// <summary>
|
||||
/// BHD
|
||||
/// </summary>
|
||||
BHD,
|
||||
/// <summary>
|
||||
/// BIF
|
||||
/// </summary>
|
||||
BIF,
|
||||
/// <summary>
|
||||
/// BMD
|
||||
/// </summary>
|
||||
BMD,
|
||||
/// <summary>
|
||||
/// BND
|
||||
/// </summary>
|
||||
BND,
|
||||
/// <summary>
|
||||
/// BOB
|
||||
/// </summary>
|
||||
BOB,
|
||||
/// <summary>
|
||||
/// BRL
|
||||
/// </summary>
|
||||
BRL,
|
||||
/// <summary>
|
||||
/// BSD
|
||||
/// </summary>
|
||||
BSD,
|
||||
/// <summary>
|
||||
/// BTN
|
||||
/// </summary>
|
||||
BTN,
|
||||
/// <summary>
|
||||
/// BWP
|
||||
/// </summary>
|
||||
BWP,
|
||||
/// <summary>
|
||||
/// BYN
|
||||
/// </summary>
|
||||
BYN,
|
||||
/// <summary>
|
||||
/// BZD
|
||||
/// </summary>
|
||||
BZD,
|
||||
/// <summary>
|
||||
/// CAD
|
||||
/// </summary>
|
||||
CAD,
|
||||
/// <summary>
|
||||
/// CDF
|
||||
/// </summary>
|
||||
CDF,
|
||||
/// <summary>
|
||||
/// CHF
|
||||
/// </summary>
|
||||
CHF,
|
||||
/// <summary>
|
||||
/// CLP
|
||||
/// </summary>
|
||||
CLP,
|
||||
/// <summary>
|
||||
/// CNY
|
||||
/// </summary>
|
||||
CNY,
|
||||
/// <summary>
|
||||
/// COP
|
||||
/// </summary>
|
||||
COP,
|
||||
/// <summary>
|
||||
/// CRC
|
||||
/// </summary>
|
||||
CRC,
|
||||
/// <summary>
|
||||
/// CUP
|
||||
/// </summary>
|
||||
CUP,
|
||||
/// <summary>
|
||||
/// CVE
|
||||
/// </summary>
|
||||
CVE,
|
||||
/// <summary>
|
||||
/// CZK
|
||||
/// </summary>
|
||||
CZK,
|
||||
/// <summary>
|
||||
/// DJF
|
||||
/// </summary>
|
||||
DJF,
|
||||
/// <summary>
|
||||
/// DKK
|
||||
/// </summary>
|
||||
DKK,
|
||||
/// <summary>
|
||||
/// DOP
|
||||
/// </summary>
|
||||
DOP,
|
||||
/// <summary>
|
||||
/// DZD
|
||||
/// </summary>
|
||||
DZD,
|
||||
/// <summary>
|
||||
/// EGP
|
||||
/// </summary>
|
||||
EGP,
|
||||
/// <summary>
|
||||
/// ERN
|
||||
/// </summary>
|
||||
ERN,
|
||||
/// <summary>
|
||||
/// ETB
|
||||
/// </summary>
|
||||
ETB,
|
||||
/// <summary>
|
||||
/// EUR
|
||||
/// </summary>
|
||||
EUR,
|
||||
/// <summary>
|
||||
/// FJD
|
||||
/// </summary>
|
||||
FJD,
|
||||
/// <summary>
|
||||
/// FKP
|
||||
/// </summary>
|
||||
FKP,
|
||||
/// <summary>
|
||||
/// GBP
|
||||
/// </summary>
|
||||
GBP,
|
||||
/// <summary>
|
||||
/// GEL
|
||||
/// </summary>
|
||||
GEL,
|
||||
/// <summary>
|
||||
/// GHS
|
||||
/// </summary>
|
||||
GHS,
|
||||
/// <summary>
|
||||
/// GIP
|
||||
/// </summary>
|
||||
GIP,
|
||||
/// <summary>
|
||||
/// GMD
|
||||
/// </summary>
|
||||
GMD,
|
||||
/// <summary>
|
||||
/// GNF
|
||||
/// </summary>
|
||||
GNF,
|
||||
/// <summary>
|
||||
/// GTQ
|
||||
/// </summary>
|
||||
GTQ,
|
||||
/// <summary>
|
||||
/// GYD
|
||||
/// </summary>
|
||||
GYD,
|
||||
/// <summary>
|
||||
/// HKD
|
||||
/// </summary>
|
||||
HKD,
|
||||
/// <summary>
|
||||
/// HNL
|
||||
/// </summary>
|
||||
HNL,
|
||||
/// <summary>
|
||||
/// HRK
|
||||
/// </summary>
|
||||
HRK,
|
||||
/// <summary>
|
||||
/// HTG
|
||||
/// </summary>
|
||||
HTG,
|
||||
/// <summary>
|
||||
/// HUF
|
||||
/// </summary>
|
||||
HUF,
|
||||
/// <summary>
|
||||
/// IDR
|
||||
/// </summary>
|
||||
IDR,
|
||||
/// <summary>
|
||||
/// ILS
|
||||
/// </summary>
|
||||
ILS,
|
||||
/// <summary>
|
||||
/// INR
|
||||
/// </summary>
|
||||
INR,
|
||||
/// <summary>
|
||||
/// IQD
|
||||
/// </summary>
|
||||
IQD,
|
||||
/// <summary>
|
||||
/// IRR
|
||||
/// </summary>
|
||||
IRR,
|
||||
/// <summary>
|
||||
/// ISK
|
||||
/// </summary>
|
||||
ISK,
|
||||
/// <summary>
|
||||
/// JMD
|
||||
/// </summary>
|
||||
JMD,
|
||||
/// <summary>
|
||||
/// JOD
|
||||
/// </summary>
|
||||
JOD,
|
||||
/// <summary>
|
||||
/// JPY
|
||||
/// </summary>
|
||||
JPY,
|
||||
/// <summary>
|
||||
/// KES
|
||||
/// </summary>
|
||||
KES,
|
||||
/// <summary>
|
||||
/// KGS
|
||||
/// </summary>
|
||||
KGS,
|
||||
/// <summary>
|
||||
/// KHR
|
||||
/// </summary>
|
||||
KHR,
|
||||
/// <summary>
|
||||
/// KMF
|
||||
/// </summary>
|
||||
KMF,
|
||||
/// <summary>
|
||||
/// KPW
|
||||
/// </summary>
|
||||
KPW,
|
||||
/// <summary>
|
||||
/// KRW
|
||||
/// </summary>
|
||||
KRW,
|
||||
/// <summary>
|
||||
/// KWD
|
||||
/// </summary>
|
||||
KWD,
|
||||
/// <summary>
|
||||
/// KYD
|
||||
/// </summary>
|
||||
KYD,
|
||||
/// <summary>
|
||||
/// KZT
|
||||
/// </summary>
|
||||
KZT,
|
||||
/// <summary>
|
||||
/// LAK
|
||||
/// </summary>
|
||||
LAK,
|
||||
/// <summary>
|
||||
/// LBP
|
||||
/// </summary>
|
||||
LBP,
|
||||
/// <summary>
|
||||
/// LKR
|
||||
/// </summary>
|
||||
LKR,
|
||||
/// <summary>
|
||||
/// LRD
|
||||
/// </summary>
|
||||
LRD,
|
||||
/// <summary>
|
||||
/// LSL
|
||||
/// </summary>
|
||||
LSL,
|
||||
/// <summary>
|
||||
/// LYD
|
||||
/// </summary>
|
||||
LYD,
|
||||
/// <summary>
|
||||
/// MAD
|
||||
/// </summary>
|
||||
MAD,
|
||||
/// <summary>
|
||||
/// MDL
|
||||
/// </summary>
|
||||
MDL,
|
||||
/// <summary>
|
||||
/// MGA
|
||||
/// </summary>
|
||||
MGA,
|
||||
/// <summary>
|
||||
/// MKD
|
||||
/// </summary>
|
||||
MKD,
|
||||
/// <summary>
|
||||
/// MMK
|
||||
/// </summary>
|
||||
MMK,
|
||||
/// <summary>
|
||||
/// MNT
|
||||
/// </summary>
|
||||
MNT,
|
||||
/// <summary>
|
||||
/// MOP
|
||||
/// </summary>
|
||||
MOP,
|
||||
/// <summary>
|
||||
/// MRU
|
||||
/// </summary>
|
||||
MRU,
|
||||
/// <summary>
|
||||
/// MUR
|
||||
/// </summary>
|
||||
MUR,
|
||||
/// <summary>
|
||||
/// MVR
|
||||
/// </summary>
|
||||
MVR,
|
||||
/// <summary>
|
||||
/// MWK
|
||||
/// </summary>
|
||||
MWK,
|
||||
/// <summary>
|
||||
/// MXN
|
||||
/// </summary>
|
||||
MXN,
|
||||
/// <summary>
|
||||
/// MYR
|
||||
/// </summary>
|
||||
MYR,
|
||||
/// <summary>
|
||||
/// MZN
|
||||
/// </summary>
|
||||
MZN,
|
||||
/// <summary>
|
||||
/// NAD
|
||||
/// </summary>
|
||||
NAD,
|
||||
/// <summary>
|
||||
/// NGN
|
||||
/// </summary>
|
||||
NGN,
|
||||
/// <summary>
|
||||
/// NIO
|
||||
/// </summary>
|
||||
NIO,
|
||||
/// <summary>
|
||||
/// NOK
|
||||
/// </summary>
|
||||
NOK,
|
||||
/// <summary>
|
||||
/// NPR
|
||||
/// </summary>
|
||||
NPR,
|
||||
/// <summary>
|
||||
/// NZD
|
||||
/// </summary>
|
||||
NZD,
|
||||
/// <summary>
|
||||
/// OMR
|
||||
/// </summary>
|
||||
OMR,
|
||||
/// <summary>
|
||||
/// PAB
|
||||
/// </summary>
|
||||
PAB,
|
||||
/// <summary>
|
||||
/// PEN
|
||||
/// </summary>
|
||||
PEN,
|
||||
/// <summary>
|
||||
/// PGK
|
||||
/// </summary>
|
||||
PGK,
|
||||
/// <summary>
|
||||
/// PHP
|
||||
/// </summary>
|
||||
PHP,
|
||||
/// <summary>
|
||||
/// PKR
|
||||
/// </summary>
|
||||
PKR,
|
||||
/// <summary>
|
||||
/// PLN
|
||||
/// </summary>
|
||||
PLN,
|
||||
/// <summary>
|
||||
/// PYG
|
||||
/// </summary>
|
||||
PYG,
|
||||
/// <summary>
|
||||
/// QAR
|
||||
/// </summary>
|
||||
QAR,
|
||||
/// <summary>
|
||||
/// RON
|
||||
/// </summary>
|
||||
RON,
|
||||
/// <summary>
|
||||
/// RSD
|
||||
/// </summary>
|
||||
RSD,
|
||||
/// <summary>
|
||||
/// RUB
|
||||
/// </summary>
|
||||
RUB,
|
||||
/// <summary>
|
||||
/// RWF
|
||||
/// </summary>
|
||||
RWF,
|
||||
/// <summary>
|
||||
/// SAR
|
||||
/// </summary>
|
||||
SAR,
|
||||
/// <summary>
|
||||
/// SBD
|
||||
/// </summary>
|
||||
SBD,
|
||||
/// <summary>
|
||||
/// SCR
|
||||
/// </summary>
|
||||
SCR,
|
||||
/// <summary>
|
||||
/// SDG
|
||||
/// </summary>
|
||||
SDG,
|
||||
/// <summary>
|
||||
/// SEK
|
||||
/// </summary>
|
||||
SEK,
|
||||
/// <summary>
|
||||
/// SGD
|
||||
/// </summary>
|
||||
SGD,
|
||||
/// <summary>
|
||||
/// SHP
|
||||
/// </summary>
|
||||
SHP,
|
||||
/// <summary>
|
||||
/// SLL
|
||||
/// </summary>
|
||||
SLL,
|
||||
/// <summary>
|
||||
/// SOS
|
||||
/// </summary>
|
||||
SOS,
|
||||
/// <summary>
|
||||
/// SRD
|
||||
/// </summary>
|
||||
SRD,
|
||||
/// <summary>
|
||||
/// SSP
|
||||
/// </summary>
|
||||
SSP,
|
||||
/// <summary>
|
||||
/// STD
|
||||
/// </summary>
|
||||
STD,
|
||||
/// <summary>
|
||||
/// SYP
|
||||
/// </summary>
|
||||
SYP,
|
||||
/// <summary>
|
||||
/// SZL
|
||||
/// </summary>
|
||||
SZL,
|
||||
/// <summary>
|
||||
/// THB
|
||||
/// </summary>
|
||||
THB,
|
||||
/// <summary>
|
||||
/// TJS
|
||||
/// </summary>
|
||||
TJS,
|
||||
/// <summary>
|
||||
/// TMT
|
||||
/// </summary>
|
||||
TMT,
|
||||
/// <summary>
|
||||
/// TND
|
||||
/// </summary>
|
||||
TND,
|
||||
/// <summary>
|
||||
/// TOP
|
||||
/// </summary>
|
||||
TOP,
|
||||
/// <summary>
|
||||
/// TRY
|
||||
/// </summary>
|
||||
TRY,
|
||||
/// <summary>
|
||||
/// TTD
|
||||
/// </summary>
|
||||
TTD,
|
||||
/// <summary>
|
||||
/// TWD
|
||||
/// </summary>
|
||||
TWD,
|
||||
/// <summary>
|
||||
/// TZS
|
||||
/// </summary>
|
||||
TZS,
|
||||
/// <summary>
|
||||
/// UAH
|
||||
/// </summary>
|
||||
UAH,
|
||||
/// <summary>
|
||||
/// UGX
|
||||
/// </summary>
|
||||
UGX,
|
||||
/// <summary>
|
||||
/// USD
|
||||
/// </summary>
|
||||
USD,
|
||||
/// <summary>
|
||||
/// UYU
|
||||
/// </summary>
|
||||
UYU,
|
||||
/// <summary>
|
||||
/// UZS
|
||||
/// </summary>
|
||||
UZS,
|
||||
/// <summary>
|
||||
/// VEF
|
||||
/// </summary>
|
||||
VEF,
|
||||
/// <summary>
|
||||
/// VND
|
||||
/// </summary>
|
||||
VND,
|
||||
/// <summary>
|
||||
/// VUV
|
||||
/// </summary>
|
||||
VUV,
|
||||
/// <summary>
|
||||
/// WST
|
||||
/// </summary>
|
||||
WST,
|
||||
/// <summary>
|
||||
/// XAF
|
||||
/// </summary>
|
||||
XAF,
|
||||
/// <summary>
|
||||
/// XCD
|
||||
/// </summary>
|
||||
XCD,
|
||||
/// <summary>
|
||||
/// XOF
|
||||
/// </summary>
|
||||
XOF,
|
||||
/// <summary>
|
||||
/// XPF
|
||||
/// </summary>
|
||||
XPF,
|
||||
/// <summary>
|
||||
/// YER
|
||||
/// </summary>
|
||||
YER,
|
||||
/// <summary>
|
||||
/// ZAR
|
||||
/// </summary>
|
||||
ZAR,
|
||||
/// <summary>
|
||||
/// ZMW
|
||||
/// </summary>
|
||||
ZMW,
|
||||
/// <summary>
|
||||
/// ZWL
|
||||
/// </summary>
|
||||
ZWL
|
||||
}
|
||||
}
|
||||
28
Models/CurrencyValue.cs
Normal file
28
Models/CurrencyValue.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
|
||||
namespace NejCommon.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// A value with a currency
|
||||
/// </summary>
|
||||
public class CurrencyValue
|
||||
{
|
||||
public CurrencyValue()
|
||||
{
|
||||
Currency = Currency.EUR;
|
||||
Value = 0;
|
||||
}
|
||||
public CurrencyValue(Currency currency, decimal value)
|
||||
{
|
||||
Value = value;
|
||||
Currency = currency;
|
||||
}
|
||||
public decimal Value { get; set; }
|
||||
public Currency Currency { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Value} {Currency}";
|
||||
}
|
||||
}
|
||||
}
|
||||
269
Models/ModelBinder.cs
Normal file
269
Models/ModelBinder.cs
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
using AngleSharp.Dom;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using NejAccountingAPI.Documents;
|
||||
using NejAccountingAPI.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace NejCommon.Models
|
||||
{
|
||||
public abstract class RouteValue
|
||||
{
|
||||
public string Name { get; }
|
||||
public bool Primary { get; } = false;
|
||||
|
||||
public RouteValue(string name, bool primary = false)
|
||||
{
|
||||
Name = name;
|
||||
Primary = primary;
|
||||
}
|
||||
}
|
||||
|
||||
public class RouteValue<TEntity> : RouteValue where TEntity : class
|
||||
{
|
||||
public Expression<Func<TEntity, string, bool>> Expression { get; }
|
||||
|
||||
public RouteValue(string name, Expression<Func<TEntity, string, bool>> expression) : base(name)
|
||||
{
|
||||
Expression = expression;
|
||||
}
|
||||
public RouteValue(string name, bool primary, Expression<Func<TEntity, string, bool>> expression) : base(name, primary)
|
||||
{
|
||||
Expression = expression;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public abstract class EntityBinder
|
||||
{
|
||||
}
|
||||
|
||||
public abstract class EntityBinder<TEntity> : EntityBinder<TEntity, string> where TEntity : class
|
||||
{
|
||||
public EntityBinder(AppDbContext db, RouteValue<TEntity>[] routeValues) : base(db, routeValues)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//id should be Guid, int, string, etc.
|
||||
public abstract class EntityBinder<TEntity, TIdType> : EntityBinder, IModelBinder
|
||||
where TEntity : class
|
||||
where TIdType : IEquatable<TIdType>
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly RouteValue<TEntity>[] _routeValues;
|
||||
|
||||
public RouteValue<TEntity>[] RouteValues => _routeValues;
|
||||
|
||||
|
||||
public EntityBinder(AppDbContext db, RouteValue<TEntity>[] routeValues)
|
||||
{
|
||||
_db = db;
|
||||
_routeValues = routeValues;
|
||||
}
|
||||
|
||||
public async Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
{
|
||||
if (bindingContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(bindingContext));
|
||||
}
|
||||
|
||||
//get the binding parameter attributes
|
||||
|
||||
// Fetch the route values from the route data
|
||||
var capturedRouteData = new Dictionary<string, object>();
|
||||
foreach (var routeValue in _routeValues)
|
||||
{
|
||||
var name = routeValue.Name;
|
||||
if (routeValue.Primary && !string.IsNullOrWhiteSpace(bindingContext.ModelName))
|
||||
{
|
||||
name = bindingContext.ModelName;
|
||||
}
|
||||
|
||||
/*
|
||||
Console.WriteLine("Route value: " + name);
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(bindingContext.ActionContext.RouteData.Values[name]);
|
||||
*/
|
||||
|
||||
var value = bindingContext.ValueProvider.GetValue(name).FirstOrDefault();//bindingContext.ActionContext.RouteData.Values[name];
|
||||
|
||||
if (value is null)
|
||||
{
|
||||
bindingContext.ModelState.AddModelError(name, "Route value not found");
|
||||
}
|
||||
else
|
||||
{
|
||||
capturedRouteData[routeValue.Name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all the required route values are present
|
||||
if (!bindingContext.ModelState.IsValid)
|
||||
{
|
||||
bindingContext.Result = ModelBindingResult.Failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the entity with the specified route values
|
||||
var query = _db.Set<TEntity>().AsQueryable();
|
||||
foreach (var routeValue in _routeValues)
|
||||
{
|
||||
var value = capturedRouteData[routeValue.Name];
|
||||
|
||||
var par = Expression.Parameter(typeof(TEntity));
|
||||
var exp = Expression.Invoke(routeValue.Expression, par, Expression.Constant((string)value));
|
||||
var redExp = exp.Reduce();
|
||||
var compiledExp = Expression.Lambda<Func<TEntity, bool>>(redExp, par);
|
||||
|
||||
query = query.Where(compiledExp);
|
||||
}
|
||||
|
||||
var model = await query.FirstOrDefaultAsync();
|
||||
|
||||
if (model == null)
|
||||
{
|
||||
bindingContext.HttpContext.Response.StatusCode = 404;
|
||||
bindingContext.ModelState.AddModelError(bindingContext.FieldName, "Not found in DB");
|
||||
bindingContext.Result = ModelBindingResult.Failed();
|
||||
return;
|
||||
}
|
||||
bindingContext.Result = ModelBindingResult.Success(model);
|
||||
bindingContext.ValidationState[bindingContext.Result] = new ValidationStateEntry
|
||||
{
|
||||
SuppressValidation = true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class EntityBinderOperationFilter : IOperationFilter
|
||||
{
|
||||
public object CreateDummyInstance(Type entityBinderType)
|
||||
{
|
||||
var constructors = entityBinderType.GetConstructors();
|
||||
var constructor = constructors.OrderBy(x => x.GetParameters().Count()).First();
|
||||
var constructorParameters = constructor.GetParameters();
|
||||
|
||||
var entityBinder = (EntityBinder)constructor.Invoke(constructorParameters.Select(x => (object?)null).ToArray());
|
||||
return entityBinder;
|
||||
}
|
||||
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
var actionDescriptor = context.ApiDescription.ActionDescriptor;
|
||||
|
||||
// Get the action parameters with a ModelBinder attribute
|
||||
var modelBinderParameters = actionDescriptor.Parameters
|
||||
.Where(p => p.BindingInfo?.BinderType != null && typeof(EntityBinder).IsAssignableFrom(p.BindingInfo.BinderType))
|
||||
.Where(x => x != null)
|
||||
.ToList();
|
||||
if (modelBinderParameters.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//Console.WriteLine("Applying EntityBinderOperationFilter to: " + actionDescriptor.DisplayName);
|
||||
|
||||
operation.Parameters = operation.Parameters.Where(p => !modelBinderParameters.Any(mp => mp.Name == p.Name)).ToList();
|
||||
|
||||
/*
|
||||
// Get the EntityBinder RouteValues
|
||||
foreach (var parameter in modelBinderParameters)
|
||||
{
|
||||
var entityBinderType = parameter.BindingInfo!.BinderType;
|
||||
var routeValuesProperty = entityBinderType!.GetProperty("RouteValues");
|
||||
if (routeValuesProperty == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var routeValues = (IEnumerable<RouteValue>?)routeValuesProperty.GetValue(CreateDummyInstance(entityBinderType));
|
||||
if (routeValues == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach (var routeValue in routeValues)
|
||||
{
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = routeValue.Name,
|
||||
In = ParameterLocation.Path,
|
||||
Required = true,
|
||||
Schema = new OpenApiSchema
|
||||
{
|
||||
Type = "string"
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}*/
|
||||
|
||||
// Exclude the EntityBinder parameter types from the document schemas
|
||||
var schemaRep = context.SchemaRepository;
|
||||
foreach (var parameter in modelBinderParameters)
|
||||
{
|
||||
var entityBinderType = parameter.BindingInfo?.BinderType;
|
||||
|
||||
// Get the EntityBinder generic type argument
|
||||
var entityType = GetBinderEntityType(entityBinderType);
|
||||
if (entityType == null)
|
||||
{
|
||||
Console.WriteLine("Couldn't find entityType of: " + entityBinderType);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (schemaRep.Schemas.ContainsKey(entityType.Name))
|
||||
{
|
||||
//Console.WriteLine("Removing schema: " + entityType.Name);
|
||||
schemaRep.Schemas.Remove(entityType.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Type? GetBinderEntityType(Type? binderType)
|
||||
{
|
||||
if (binderType == null)
|
||||
return null;
|
||||
if (binderType.IsGenericType && binderType.GetGenericTypeDefinition() == typeof(EntityBinder<>))
|
||||
{
|
||||
return binderType.GetTypeInfo().GetGenericArguments().FirstOrDefault();
|
||||
}
|
||||
return GetBinderEntityType(binderType.BaseType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class EntityAttribute<TEntityBinder, TEntity> : Attribute, IPropertyValidationFilter, IApiDescriptionVisibilityProvider, IBinderTypeProviderMetadata where TEntityBinder : EntityBinder<TEntity> where TEntity : class
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Type? BinderType => typeof(TEntityBinder);
|
||||
|
||||
/// <inheritdoc />
|
||||
public BindingSource? BindingSource => BindingSource.Custom;
|
||||
|
||||
public bool IgnoreApi => true;
|
||||
}
|
||||
}
|
||||
23
Services/Email/IEmailService.cs
Normal file
23
Services/Email/IEmailService.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using System.Net.Mail;
|
||||
using System.Reflection;
|
||||
using NejAccountingAPI.Emails;
|
||||
|
||||
namespace NejAccountingAPI.Services.Email;
|
||||
|
||||
public interface IEmailService
|
||||
{
|
||||
Task SendEmailAsync(string recipient, string subject, string message, List<Attachment>? attachments = null);
|
||||
|
||||
async Task SendDocumentAsync<T, T2>(string recipient, T2 data, List<Attachment>? attachments = null) where T : EmailBase<T, T2>
|
||||
{
|
||||
var func = typeof(T).GetMethod("GetHTML", BindingFlags.Static | BindingFlags.FlattenHierarchy | BindingFlags.Public);
|
||||
var obj = Activator.CreateInstance<T>();
|
||||
obj.Data = data;
|
||||
var subject = obj.GetSubject();
|
||||
|
||||
//invoke the GetHTML function asynchronously and wait for the result
|
||||
var html = await (Task<string>)func.Invoke(null, new object[] { data });
|
||||
|
||||
await SendEmailAsync(recipient, subject, html, attachments);
|
||||
}
|
||||
}
|
||||
78
Services/Email/SMTPService.cs
Normal file
78
Services/Email/SMTPService.cs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
using System.Net.Mail;
|
||||
using MailKit;
|
||||
using MailKit.Net.Imap;
|
||||
using MailKit.Net.Smtp;
|
||||
using MimeKit;
|
||||
|
||||
namespace NejAccountingAPI.Services.Email;
|
||||
|
||||
public class SMTPService : IEmailService
|
||||
{
|
||||
public record SMTPSettings(string From, string Host, int Port, int ImapPort, string Username, string Password);
|
||||
|
||||
private readonly SMTPSettings _settings;
|
||||
|
||||
private readonly ILogger<SMTPService> Logger;
|
||||
|
||||
public SMTPService(ILogger<SMTPService> logger, SMTPSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public async Task SendEmailAsync(string recipient, string subject, string message, List<Attachment>? attachments = null)
|
||||
{
|
||||
var mess = new MimeMessage();
|
||||
mess.From.Add(MailboxAddress.Parse(_settings.From));
|
||||
mess.To.Add(MailboxAddress.Parse(recipient));
|
||||
mess.Subject = subject;
|
||||
|
||||
var bodyBuilder = new BodyBuilder();
|
||||
bodyBuilder.HtmlBody = message;
|
||||
bodyBuilder.TextBody = "";
|
||||
|
||||
foreach (var attachment in attachments ?? new List<Attachment>())
|
||||
bodyBuilder.Attachments.Add(attachment.Name, attachment.ContentStream, ContentType.Parse(attachment.ContentType.ToString()));
|
||||
|
||||
mess.Body = bodyBuilder.ToMessageBody();
|
||||
|
||||
using (var client = new MailKit.Net.Smtp.SmtpClient())
|
||||
{
|
||||
await client.ConnectAsync(_settings.Host, _settings.Port);
|
||||
|
||||
// Note: only needed if the SMTP server requires authentication
|
||||
await client.AuthenticateAsync(_settings.Username, _settings.Password);
|
||||
|
||||
await client.SendAsync(mess);
|
||||
await client.DisconnectAsync(true);
|
||||
|
||||
using (var imap = new MailKit.Net.Imap.ImapClient())
|
||||
{
|
||||
await imap.ConnectAsync(_settings.Host, _settings.ImapPort); // or ConnectSSL for SSL
|
||||
await imap.AuthenticateAsync(_settings.Username, _settings.Password);
|
||||
|
||||
//get the sent folder
|
||||
IMailFolder sent = null;
|
||||
if (imap.Capabilities.HasFlag(ImapCapabilities.SpecialUse))
|
||||
sent = imap.GetFolder(SpecialFolder.Sent);
|
||||
|
||||
if (sent == null)
|
||||
{
|
||||
// get the default personal namespace root folder
|
||||
var personal = imap.GetFolder(imap.PersonalNamespaces[0]);
|
||||
|
||||
// This assumes the sent folder's name is "Sent", but use whatever the real name is
|
||||
sent = await personal.GetSubfolderAsync("Sent").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (sent != null)
|
||||
{
|
||||
await sent.AppendAsync(mess, MessageFlags.Seen);
|
||||
}
|
||||
|
||||
await imap.DisconnectAsync(true);
|
||||
}
|
||||
Logger.LogInformation("Email {0} sent to {1} - {2} attachments", subject, recipient, attachments?.Count ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Utils/DateTimeConverter.cs
Normal file
18
Utils/DateTimeConverter.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NejCommon.Utils
|
||||
{
|
||||
public class DateOnlyConverter : JsonConverter<DateOnly>
|
||||
{
|
||||
public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return DateOnly.Parse(reader.GetString() ?? string.Empty);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString("yyyy-MM-dd"));
|
||||
}
|
||||
}
|
||||
}
|
||||
119
Utils/Extensions.cs
Normal file
119
Utils/Extensions.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
using System.Globalization;
|
||||
using System.Linq.Expressions;
|
||||
using AutoMapPropertyHelper;
|
||||
using EntityFrameworkCore.Projectables;
|
||||
using EntityFrameworkCore.Projectables.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NejAccountingAPI.Models;
|
||||
using NejCommon.Controllers;
|
||||
using NejCommon.Models;
|
||||
|
||||
namespace NejCommon.Utils;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static IQueryable<TType> ApplyPagination<TType>(this IQueryable<TType> query, Pagination pag)
|
||||
{
|
||||
query = query.Skip(pag.Offset);
|
||||
query = query.Take(pag.Count);
|
||||
return query;
|
||||
}
|
||||
|
||||
public static async Task<PaginationResponse<TResponseType>> ApplyPaginationRes<TType, TResponseType>(this IQueryable<TType> query, IServiceProvider providers, Pagination pag) where TResponseType : IAutomappedAttribute<TType, TResponseType>, new()
|
||||
{
|
||||
var totalCount = await query.CountAsync();
|
||||
query = query.Skip(pag.Offset);
|
||||
query = query.Take(pag.Count);
|
||||
var projector = new TResponseType().GetProjectorFrom(providers);
|
||||
|
||||
return new PaginationResponse<TResponseType>
|
||||
{
|
||||
TotalCount = totalCount,
|
||||
Offset = pag.Offset,
|
||||
Count = pag.Count,
|
||||
Data = query.Select(projector).AsAsyncEnumerable()
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<PaginationResponse<TResponseType>> ApplySearchPaginationRes<TType, TResponseType, TEntityBinder>(this IQueryable<TType> query, string? search, IServiceProvider providers, Pagination pag, List<Expression<Func<TType, string, bool>>> matchers)
|
||||
where TType : class
|
||||
where TResponseType : IAutomappedAttribute<TType, TResponseType>, new()
|
||||
where TEntityBinder : EntityBinder<TType>
|
||||
{
|
||||
if (search != null)
|
||||
{
|
||||
var searchers = new List<Expression<Func<TType, bool>>>();
|
||||
foreach (var matcher in matchers)
|
||||
{
|
||||
//reduce expression from TType, string, bool to TType, bool by passing the search string
|
||||
|
||||
var par = Expression.Parameter(typeof(TType));
|
||||
var exp = Expression.Invoke(matcher, par, Expression.Constant(search));
|
||||
var redExp = exp.Reduce();
|
||||
var reducedMatcher = Expression.Lambda<Func<TType, bool>>(redExp, par);
|
||||
|
||||
searchers.Add(reducedMatcher);
|
||||
}
|
||||
|
||||
var extPar = Expression.Parameter(typeof(TType));
|
||||
var binder = (TEntityBinder)typeof(TEntityBinder).GetConstructor(new[] { typeof(AppDbContext) })!.Invoke(new object?[] { null });
|
||||
var idMatcher = binder.RouteValues.First(x => x.Primary).Expression;
|
||||
var reducedIdMatcher = Expression.Lambda<Func<TType, bool>>(Expression.Invoke(idMatcher, extPar, Expression.Constant(search)).Reduce(), extPar);
|
||||
//Console.Writeline(reducedIdMatcher);
|
||||
searchers.Add(reducedIdMatcher);
|
||||
|
||||
// Create an expression that ORs all the searchers
|
||||
var agrPar = Expression.Parameter(typeof(TType));
|
||||
|
||||
var orExp = searchers.Aggregate((Expression)null, (current, searcher) =>
|
||||
{
|
||||
var body = Expression.Invoke(searcher, agrPar);
|
||||
if (current == null)
|
||||
{
|
||||
return body;
|
||||
}
|
||||
return Expression.OrElse(current, body);
|
||||
});
|
||||
|
||||
//reduce the epxression as much as possible
|
||||
while (orExp.CanReduce)
|
||||
{
|
||||
orExp = orExp.Reduce();
|
||||
}
|
||||
|
||||
var orLambda = Expression.Lambda<Func<TType, bool>>(orExp, agrPar);
|
||||
//Console.Writeline(orLambda);
|
||||
query = query.Where(orLambda);
|
||||
}
|
||||
|
||||
return await query.ApplyPaginationRes<TType, TResponseType>(providers, pag);
|
||||
}
|
||||
|
||||
[Projectable]
|
||||
public static IQueryable<TType> ApplyDateOnlyFrameQ<TType>(this IQueryable<TType> query, Func<TType, DateOnly> dateAcessor, DateOnlyFrame frame) => query.Where(e => dateAcessor(e) >= frame.fromDate && dateAcessor(e) <= frame.toDate);
|
||||
[Projectable]
|
||||
public static IEnumerable<TType> ApplyDateOnlyFrameE<TType>(this ICollection<TType> query, Func<TType, DateOnly> dateAcessor, DateOnlyFrame frame) => query.Where(e => dateAcessor(e) >= frame.fromDate && dateAcessor(e) <= frame.toDate);
|
||||
|
||||
[Projectable]
|
||||
public static IQueryable<TType> ApplyDateTimeFrameQ<TType>(this IQueryable<TType> query, string datePropery, DateTimeFrame frame) => query.Where(e => EF.Property<DateTime>(e, datePropery) >= frame.FromDate && EF.Property<DateTime>(e, datePropery) <= frame.ToDate);
|
||||
[Projectable]
|
||||
public static IEnumerable<TType> ApplyDateTimeFrameE<TType>(this ICollection<TType> query, Func<TType, DateTime> dateAcessor, DateTimeFrame frame) => query.Where(e => dateAcessor(e) >= frame.FromDate && dateAcessor(e) <= frame.ToDate);
|
||||
|
||||
[Projectable]
|
||||
public static DateOnly ToDateOnly(this DateTime date) => new DateOnly(date.Year, date.Month, date.Day);
|
||||
[Projectable]
|
||||
public static DateTime ToDateTime(this DateOnly date) => new DateTime(date.Year, date.Month, date.Day);
|
||||
[Projectable]
|
||||
public static decimal Round(this decimal value, int decimals = 2) => Math.Round(value, decimals);
|
||||
[Projectable]
|
||||
public static decimal Floor(this decimal value, int decimals = 2) => Math.Floor(value * (decimal)Math.Pow(10, decimals)) / (decimal)Math.Pow(10, decimals);
|
||||
[Projectable]
|
||||
public static int RountToInt(this decimal value) => (int)Math.Round(value);
|
||||
|
||||
[Projectable]
|
||||
public static int GetInt(this string value, NumberStyles? style = null, CultureInfo? culture = null) => int.Parse(value, style ?? NumberStyles.Number, culture ?? CultureInfo.InvariantCulture);
|
||||
[Projectable]
|
||||
public static decimal GetDecimal(this string value, NumberStyles? style = null, CultureInfo? culture = null) => decimal.Parse(value, style ?? NumberStyles.Number, culture ?? CultureInfo.InvariantCulture);
|
||||
[Projectable]
|
||||
public static float GetFloat(this string value, NumberStyles? style = null, CultureInfo? culture = null) => float.Parse(value, style ?? NumberStyles.Number, culture ?? CultureInfo.InvariantCulture);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user