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