OnlineSalesAutoCrop/Api/OnlineSalesAutoCrop.CoreAPI/Startup.cs

533 lines
14 KiB
C#
Raw Normal View History

2026-06-14 12:46:29 +06:00
using Asp.Versioning;
using Asp.Versioning.ApiExplorer;
using OnlineSalesAutoCrop.CoreAPI.API.Swagger;
using OnlineSalesAutoCrop.CoreAPI.Configuration.DI;
using OnlineSalesAutoCrop.CoreAPI.Configurations;
using OnlineSalesAutoCrop.CoreAPI.Models;
using OnlineSalesAutoCrop.CoreAPI.Models.Global;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Systems;
using OnlineSalesAutoCrop.CoreAPI.SignalRHub;
using Hangfire;
using Hangfire.SqlServer;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.CookiePolicy;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading.RateLimiting;
using System.Threading.Tasks;
namespace OnlineSalesAutoCrop.CoreAPI
{
/// <summary>
///
/// </summary>
public class Startup
{
private readonly AppSettings _appSettings;
/// <summary>
///
/// </summary>
public IConfiguration Configuration { get; }
/// <summary>
/// Initializes a new instance of the Startup class using the specified configuration settings.
/// </summary>
/// <param name="configuration">The configuration settings used to initialize the application. Cannot be null.</param>
public Startup(IConfiguration configuration)
{
Configuration = configuration;
_appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
}
2026-06-14 12:46:29 +06:00
/// <summary>
///
/// </summary>
/// <param name="services"></param>
public void ConfigureServices(IServiceCollection services)
{
try
{
if (_appSettings.IsValid())
{
#region AppSettings & MenuSettings
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
services.Configure<MenuSettings>(Configuration.GetSection("MenuSettings"));
#endregion
#region Controllers
services.AddControllers(options =>
{
options.Filters.Add(new ProducesAttribute("application/json"));
});
#endregion
#region API versioning
services.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
})
.AddMvc() // API versioning extensions for MVC Core
.AddApiExplorer(); // API version-aware API Explorer extensions
#endregion
#region CORS
string[] origins = _appSettings.CorsOrigins.Split(',');
string[] methods = _appSettings.CorsMethods.Split(',');
string[] headers = _appSettings.CorsHeaders.Split(',');
services.AddCors(options =>
{
options.DefaultPolicyName = "CORSPolicies";
options.AddDefaultPolicy(builder =>
{
builder.WithOrigins(origins)
.SetIsOriginAllowedToAllowWildcardSubdomains()
.WithMethods(methods)
.WithHeaders(headers)
.AllowCredentials();
});
});
#endregion
#region Caching
if (_appSettings.CacheType == EnumCacheType.Redis && !string.IsNullOrEmpty(_appSettings.CacheUrl))
{
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = _appSettings.CacheUrl;
});
}
else if (_appSettings.CacheType == EnumCacheType.SqlServer && !string.IsNullOrEmpty(_appSettings.CacheUrl) && !string.IsNullOrEmpty(_appSettings.CacheTableName) && !string.IsNullOrEmpty(_appSettings.CacheSchemaName))
{
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = _appSettings.CacheUrl;
options.TableName = _appSettings.CacheTableName;
options.SchemaName = _appSettings.CacheSchemaName;
options.ExpiredItemsDeletionInterval = TimeSpan.FromMinutes(5);
});
}
else
{
services.AddMemoryCache();
}
#endregion
#region Cookie Settings
services.Configure<CookiePolicyOptions>(options =>
{
options.ConsentCookieValue = "true";
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = (SameSiteMode)_appSettings.CookieSameSite;
if (_appSettings.CookieHttpOnly)
{
options.HttpOnly = HttpOnlyPolicy.Always;
}
});
#endregion
#region Session
services.AddSession(options =>
{
options.Cookie.Name = "OnlineSalesAutoCrop.Net9.Session.Id";
options.Cookie.HttpOnly = _appSettings.CookieHttpOnly;
options.Cookie.SameSite = (SameSiteMode)_appSettings.CookieSameSite;
options.IdleTimeout = TimeSpan.FromMinutes(_appSettings.CookieLifeTime);
});
#endregion
#region JWT Authentication
var key = Encoding.ASCII.GetBytes(_appSettings.JwtCryptoKey);
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ClockSkew = TimeSpan.Zero,
ValidateIssuerSigningKey = true,
ValidIssuer = _appSettings.JwtIssuer,
ValidAudience = _appSettings.JwtAudience,
ValidateIssuer = _appSettings.JwtValidateIssuer,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateAudience = _appSettings.JwtValidateAudience
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
context.Response.Headers.Append("Token-Expired", "true");
return Task.CompletedTask;
}
};
});
#endregion
#region Mvc
services.AddMvc();
#endregion
#region Forwarded Headers
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
#endregion
#region Antiforgery
services.AddAntiforgery(options =>
{
options.SuppressXFrameOptionsHeader = false;
options.HeaderName = "OnlineSalesAutoCrop.X-XSRF-TOKEN";
options.Cookie.Name = "OnlineSalesAutoCrop.X-XSRF-ANTIFORGERY";
options.Cookie.HttpOnly = _appSettings.CookieHttpOnly;
options.Cookie.SameSite = (SameSiteMode)_appSettings.CookieSameSite;
});
#endregion
#region SignalR
services.AddSignalR();
services.AddResponseCompression(opt =>
{
opt.MimeTypes = ["application/octet-stream"];
});
#endregion
#region Rate Limit
if (!string.IsNullOrEmpty(_appSettings.RLName))
{
services.AddRateLimiter(options =>
{
options.AddPolicy(_appSettings.RLName, httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.GetIpAddress(),
factory: _ => new FixedWindowRateLimiterOptions
{
QueueLimit = _appSettings.RLQueueLimit,
PermitLimit = _appSettings.RLPermitLimit,
AutoReplenishment = _appSettings.RLAutoReplenishment,
Window = TimeSpan.FromSeconds(_appSettings.RLWindowInSecond),
QueueProcessingOrder = (QueueProcessingOrder)_appSettings.RLQueueProcessingOrder
}));
options.OnRejected = (context, cancellationToken) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter = retryAfter.TotalSeconds.ToString();
}
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.HttpContext.Response.WriteAsync(text: $"Too many requests (more than {_appSettings.RLPermitLimit} calls in {_appSettings.RLWindowInSecond} seconds). Please try again later.", cancellationToken: cancellationToken);
return new ValueTask();
};
});
}
#endregion
#region Hangfire
if (!string.IsNullOrEmpty(_appSettings.HangfireDb))
{
services.AddHangfire(config =>
{
config.UseFilter(new TimeRestrictionServerFilter(startHour: 1, endHour: 7));
config.UseSqlServerStorage(_appSettings.HangfireDb, new SqlServerStorageOptions
{
DisableGlobalLocks = true,
QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true,
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5)
});
});
services.AddHangfireServer();
}
#endregion
#region SWAGGER
if (_appSettings.Swagger.Enabled)
{
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
services.AddSwaggerGen(options =>
{
options.OperationFilter<SwaggerDefaultValues>();
options.IncludeXmlComments(filePath: XmlCommentsFilePath);
});
}
#endregion
#region Business Service
services.ConfigureBusinessServices();
#endregion
}
}
catch
{
//Nothimg to do here as we are handling all exceptions in Configure method using global exception handler
}
}
/// <summary>
///
/// </summary>
/// <param name="app"></param>
/// <param name="env"></param>
/// <param name="provider"></param>
/// <param name="loggerFactory"></param>
/// <param name="logger"></param>
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider, ILoggerFactory loggerFactory, ILogger<Startup> logger)
{
logger.LogTrace("Startup::Configure");
try
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
#region Exception Handling in Production mode
app.UseExceptionHandler(a => a.Run(async context =>
{
IExceptionHandlerPathFeature feature = context.Features.Get<IExceptionHandlerPathFeature>();
Exception exception = feature.Error;
HttpStatusCode code = HttpStatusCode.InternalServerError;
if (exception is ArgumentNullException)
code = HttpStatusCode.BadRequest;
else if (exception is ArgumentException)
code = HttpStatusCode.BadRequest;
else if (exception is UnauthorizedAccessException)
code = HttpStatusCode.Unauthorized;
else if (exception is WebException && (exception as WebException).Response is HttpWebResponse response)
code = response.StatusCode;
logger.LogError($"GLOBAL ERROR HANDLER::HTTP:{code}::{exception.Message}");
var result = JsonConvert.SerializeObject(exception, Formatting.Indented);
context.Response.Clear();
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(result);
}));
app.UseHsts();
app.UseHttpsRedirection();
#endregion
}
#region CORS
app.UseCors("CORSPolicies");
#endregion
#region Use Static Files
app.UseStaticFiles();
#endregion
#region Routing
app.UseRouting();
#endregion
#region Session
app.UseSession();
#endregion
#region For Authentication
app.UseAuthentication();
#endregion
#region For Authorization
app.UseAuthorization();
#endregion
#region X-Frame-Options
app.Use(async (context, next) =>
{
if (_appSettings.CookieHttpOnly)
{
string tokens = context.Request.Cookies["OnlineSalesAutoCrop.X-XSRF-TOKEN"];
if (!string.IsNullOrEmpty(tokens))
context.Request.Headers.Append("OnlineSalesAutoCrop.X-XSRF-TOKEN", tokens);
}
await next();
});
#endregion
#region RateLimiter
if (!string.IsNullOrEmpty(_appSettings.RLName))
{
app.UseRateLimiter();
}
#endregion
#region Logger
if (!string.IsNullOrEmpty(_appSettings.LoggerPath))
{
loggerFactory.AddFile(pathFormat: $@"{_appSettings.LoggerPath}\Log.txt", minimumLevel: (LogLevel)_appSettings.LoggerMinLevel);
}
#endregion
#region Controllers and SignalR
app.UseEndpoints(endpoints =>
{
if (!string.IsNullOrEmpty(_appSettings.RLName))
{
endpoints.MapControllers().RequireRateLimiting(_appSettings.RLName);
}
else
{
endpoints.MapControllers();
}
endpoints.MapHub<NotificationHub>("/signalRHub");
});
#endregion
#region Forwarded Headers
app.UseForwardedHeaders();
#endregion
#region Request Localization
app.UseRequestLocalization();
#endregion
#region Hangfire
if (!string.IsNullOrEmpty(_appSettings.HangfireDb))
{
app.UseHangfireDashboard();
RecurringJobOptions options = new() { TimeZone = TimeZoneInfo.Local, MisfireHandling = MisfireHandlingMode.Relaxed };
}
#endregion
#region Swagger
if (_appSettings.IsValid() && _appSettings.Swagger.Enabled)
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
foreach (var description in provider.ApiVersionDescriptions)
{
string sjp = string.IsNullOrWhiteSpace(options.RoutePrefix) ? "." : "..";
options.SwaggerEndpoint($"{sjp}/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
}
});
}
#endregion
app.Run(async (context) =>
{
context.Response.ContentType = "text/html";
await context.Response.WriteAsync("<b>Ease Taskforce Api is Running</b>");
});
}
catch (Exception ex)
{
logger.LogError(ex);
}
}
/// <summary>
///
/// </summary>
static string XmlCommentsFilePath
{
get
{
var basePath = AppContext.BaseDirectory;
var fileName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name + ".xml";
return Path.Combine(basePath, fileName);
}
}
}
}