first commit

This commit is contained in:
2026-02-26 14:04:18 +07:00
parent 57ac80a666
commit 4b7236493f
92 changed files with 4999 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using MyNewProjectName.Application.Interfaces;
using MyNewProjectName.Domain.Interfaces;
using MyNewProjectName.Infrastructure.Options;
using MyNewProjectName.Infrastructure.Persistence.Context;
using MyNewProjectName.Infrastructure.Persistence.Repositories;
using MyNewProjectName.Infrastructure.Services;
namespace MyNewProjectName.Infrastructure;
/// <summary>
/// Dependency Injection for Infrastructure Layer
/// </summary>
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
// Register Options Pattern
services.Configure<DatabaseOptions>(configuration.GetSection(DatabaseOptions.SectionName));
services.Configure<JwtOptions>(configuration.GetSection(JwtOptions.SectionName));
services.Configure<RedisOptions>(configuration.GetSection(RedisOptions.SectionName));
services.Configure<SerilogOptions>(configuration.GetSection(SerilogOptions.SectionName));
// Validate required Options on startup
services.AddOptions<DatabaseOptions>()
.Bind(configuration.GetSection(DatabaseOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();
// Validate JwtOptions only if section exists (optional for now)
var jwtSection = configuration.GetSection(JwtOptions.SectionName);
if (jwtSection.Exists() && jwtSection.GetChildren().Any())
{
services.AddOptions<JwtOptions>()
.Bind(jwtSection)
.ValidateDataAnnotations()
.ValidateOnStart();
}
// Register DbContext using Options Pattern
services.AddDbContext<ApplicationDbContext>((serviceProvider, options) =>
{
var dbOptions = serviceProvider.GetRequiredService<IOptions<DatabaseOptions>>().Value;
options.UseSqlServer(
dbOptions.DefaultConnection,
b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName));
});
// Register repositories
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
services.AddScoped<IUnitOfWork, UnitOfWork>();
// Register HttpContextAccessor (required for CurrentUserService)
services.AddHttpContextAccessor();
// Register services
services.AddScoped<IDateTimeService, DateTimeService>();
services.AddScoped<ICurrentUserService, CurrentUserService>();
return services;
}
}

View File

@@ -0,0 +1,78 @@
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.Reflection;
namespace MyNewProjectName.Infrastructure.Extensions;
/// <summary>
/// Extension methods for configuring OpenTelemetry
/// </summary>
public static class OpenTelemetryExtensions
{
/// <summary>
/// Add OpenTelemetry distributed tracing
/// </summary>
public static IServiceCollection AddOpenTelemetryTracing(
this IServiceCollection services,
string serviceName = "MyNewProjectName")
{
services.AddOpenTelemetry()
.WithTracing(builder =>
{
builder
.SetResourceBuilder(ResourceBuilder
.CreateDefault()
.AddService(serviceName)
.AddAttributes(new Dictionary<string, object>
{
["service.version"] = Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion ?? "1.0.0"
}))
.AddAspNetCoreInstrumentation(options =>
{
// Capture HTTP request/response details
options.RecordException = true;
options.EnrichWithHttpRequest = (activity, request) =>
{
activity.SetTag("http.request.method", request.Method);
activity.SetTag("http.request.path", request.Path);
activity.SetTag("http.request.query_string", request.QueryString);
};
options.EnrichWithHttpResponse = (activity, response) =>
{
activity.SetTag("http.response.status_code", response.StatusCode);
};
})
.AddHttpClientInstrumentation(options =>
{
options.RecordException = true;
options.EnrichWithHttpRequestMessage = (activity, request) =>
{
activity.SetTag("http.client.request.method", request.Method?.ToString());
activity.SetTag("http.client.request.uri", request.RequestUri?.ToString());
};
})
.AddEntityFrameworkCoreInstrumentation(options =>
{
options.SetDbStatementForText = true;
options.EnrichWithIDbCommand = (activity, command) =>
{
activity.SetTag("db.command.text", command.CommandText);
};
})
.AddSource(serviceName)
// Export to console (for development)
.AddConsoleExporter()
// Export to OTLP (for production - requires OTLP collector)
.AddOtlpExporter(options =>
{
// Configure OTLP endpoint if needed
// options.Endpoint = new Uri("http://localhost:4317");
});
});
return services;
}
}

View File

@@ -0,0 +1,89 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using MyNewProjectName.Infrastructure.Options;
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Compact;
namespace MyNewProjectName.Infrastructure.Extensions;
/// <summary>
/// Extension methods for configuring Serilog
/// </summary>
public static class SerilogExtensions
{
/// <summary>
/// Configure Serilog with structured JSON logging
/// </summary>
public static IHostBuilder UseSerilogLogging(this IHostBuilder hostBuilder, IConfiguration configuration)
{
var serilogOptions = configuration.GetSection(SerilogOptions.SectionName).Get<SerilogOptions>()
?? new SerilogOptions();
var loggerConfiguration = new LoggerConfiguration()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithEnvironmentName()
.Enrich.WithMachineName()
.Enrich.WithThreadId()
.Enrich.WithProperty("Application", "MyNewProjectName");
// Set minimum level from configuration
if (Enum.TryParse<LogEventLevel>(serilogOptions.MinimumLevel, out var minLevel))
{
loggerConfiguration.MinimumLevel.Is(minLevel);
}
// Apply overrides from configuration
foreach (var overrideConfig in serilogOptions.Override)
{
if (Enum.TryParse<LogEventLevel>(overrideConfig.Value, out var overrideLevel))
{
loggerConfiguration.MinimumLevel.Override(overrideConfig.Key, overrideLevel);
}
}
// Console sink with JSON formatting (Compact JSON format)
if (serilogOptions.WriteToConsole)
{
loggerConfiguration.WriteTo.Console(new CompactJsonFormatter());
}
// File sink with rolling
if (serilogOptions.WriteToFile)
{
var rollingInterval = Enum.TryParse<RollingInterval>(serilogOptions.RollingInterval, out var interval)
? interval
: RollingInterval.Day;
loggerConfiguration.WriteTo.File(
new CompactJsonFormatter(),
serilogOptions.FilePath,
rollingInterval: rollingInterval,
retainedFileCountLimit: serilogOptions.RetainedFileCountLimit,
shared: true);
}
// Seq sink (optional)
if (!string.IsNullOrWhiteSpace(serilogOptions.SeqUrl))
{
loggerConfiguration.WriteTo.Seq(serilogOptions.SeqUrl);
}
// Elasticsearch sink (optional)
if (!string.IsNullOrWhiteSpace(serilogOptions.ElasticsearchUrl))
{
// Note: Add Serilog.Sinks.Elasticsearch package if needed
// loggerConfiguration.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri(serilogOptions.ElasticsearchUrl)));
}
Log.Logger = loggerConfiguration.CreateLogger();
return hostBuilder.UseSerilog();
}
}

View File

@@ -0,0 +1,85 @@
// using MyNewProjectName.Application.Interfaces;
// using MyNewProjectName.Domain.Entities;
// using Microsoft.Extensions.Options;
// using Microsoft.IdentityModel.Tokens;
// using System.IdentityModel.Tokens.Jwt;
// using System.Security.Claims;
// using System.Security.Cryptography;
// using System.Text;
// using MyNewProjectName.Application.Interfaces.Common;
// namespace MyNewProjectName.Infrastructure.Identity;
// public class JwtTokenGenerator : IJwtTokenGenerator
// {
// private readonly JwtSettings _jwtSettings;
// public JwtTokenGenerator(IOptions<JwtSettings> jwtOptions)
// {
// _jwtSettings = jwtOptions.Value;
// }
// public string GenerateAccessToken(User user, List<string> roles, Guid tenantId)
// {
// var tokenHandler = new JwtSecurityTokenHandler();
// var key = Encoding.UTF8.GetBytes(_jwtSettings.Secret);
// var claims = new List<Claim>
// {
// new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
// new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
// new(JwtRegisteredClaimNames.Email, user.Email ?? string.Empty),
// new("id", user.Id.ToString()),
// new("tenantId", tenantId.ToString())
// };
// foreach (var role in roles)
// {
// claims.Add(new Claim(ClaimTypes.Role, role));
// }
// var tokenDescriptor = new SecurityTokenDescriptor
// {
// Subject = new ClaimsIdentity(claims),
// Expires = DateTime.UtcNow.AddMinutes(_jwtSettings.AccessTokenExpirationMinutes),
// Issuer = _jwtSettings.Issuer,
// Audience = _jwtSettings.Audience,
// SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
// };
// var token = tokenHandler.CreateToken(tokenDescriptor);
// return tokenHandler.WriteToken(token);
// }
// public string GenerateRefreshToken()
// {
// var randomNumber = new byte[32];
// using var rng = RandomNumberGenerator.Create();
// rng.GetBytes(randomNumber);
// return Convert.ToBase64String(randomNumber);
// }
// public ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
// {
// var tokenValidationParameters = new TokenValidationParameters
// {
// ValidateAudience = false,
// ValidateIssuer = false,
// ValidateIssuerSigningKey = true,
// IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Secret)),
// ValidateLifetime = false
// };
// var tokenHandler = new JwtSecurityTokenHandler();
// var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
// if (securityToken is not JwtSecurityToken jwtSecurityToken ||
// !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
// {
// throw new SecurityTokenException("Invalid token");
// }
// return principal;
// }
// }

View File

@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\MyNewProjectName.Application\MyNewProjectName.Application.csproj" />
<ProjectReference Include="..\MyNewProjectName.Domain\MyNewProjectName.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.0" />
<!-- Serilog packages for extension methods -->
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
<!-- OpenTelemetry packages for extension methods -->
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.11.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.11.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.11.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.11.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.EntityFrameworkCore" Version="1.0.0-beta.7" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace MyNewProjectName.Infrastructure.Options;
/// <summary>
/// Database connection configuration options
/// </summary>
public class DatabaseOptions
{
public const string SectionName = "ConnectionStrings";
/// <summary>
/// Default database connection string
/// </summary>
[Required(ErrorMessage = "DefaultConnection is required")]
public string DefaultConnection { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,42 @@
using System.ComponentModel.DataAnnotations;
namespace MyNewProjectName.Infrastructure.Options;
/// <summary>
/// JWT authentication configuration options
/// </summary>
public class JwtOptions
{
public const string SectionName = "Jwt";
/// <summary>
/// Secret key for signing JWT tokens
/// </summary>
[Required(ErrorMessage = "JWT SecretKey is required")]
[MinLength(32, ErrorMessage = "JWT SecretKey must be at least 32 characters long")]
public string SecretKey { get; set; } = string.Empty;
/// <summary>
/// Token issuer
/// </summary>
[Required(ErrorMessage = "JWT Issuer is required")]
public string Issuer { get; set; } = string.Empty;
/// <summary>
/// Token audience
/// </summary>
[Required(ErrorMessage = "JWT Audience is required")]
public string Audience { get; set; } = string.Empty;
/// <summary>
/// Token expiration time in minutes
/// </summary>
[Range(1, 1440, ErrorMessage = "ExpirationInMinutes must be between 1 and 1440 (24 hours)")]
public int ExpirationInMinutes { get; set; } = 60;
/// <summary>
/// Refresh token expiration time in days
/// </summary>
[Range(1, 365, ErrorMessage = "RefreshTokenExpirationInDays must be between 1 and 365")]
public int RefreshTokenExpirationInDays { get; set; } = 7;
}

View File

@@ -0,0 +1,24 @@
namespace MyNewProjectName.Infrastructure.Options;
/// <summary>
/// Redis cache configuration options
/// </summary>
public class RedisOptions
{
public const string SectionName = "Redis";
/// <summary>
/// Redis connection string
/// </summary>
public string ConnectionString { get; set; } = string.Empty;
/// <summary>
/// Redis instance name for key prefixing
/// </summary>
public string InstanceName { get; set; } = string.Empty;
/// <summary>
/// Default cache expiration time in minutes
/// </summary>
public int DefaultExpirationInMinutes { get; set; } = 30;
}

View File

@@ -0,0 +1,54 @@
namespace MyNewProjectName.Infrastructure.Options;
/// <summary>
/// Serilog logging configuration options
/// </summary>
public class SerilogOptions
{
public const string SectionName = "Serilog";
/// <summary>
/// Minimum log level
/// </summary>
public string MinimumLevel { get; set; } = "Information";
/// <summary>
/// Override log levels for specific namespaces
/// </summary>
public Dictionary<string, string> Override { get; set; } = new();
/// <summary>
/// Write to console
/// </summary>
public bool WriteToConsole { get; set; } = true;
/// <summary>
/// Write to file
/// </summary>
public bool WriteToFile { get; set; } = true;
/// <summary>
/// File path for logs (relative to application root)
/// </summary>
public string FilePath { get; set; } = "logs/log-.txt";
/// <summary>
/// Rolling interval for log files
/// </summary>
public string RollingInterval { get; set; } = "Day";
/// <summary>
/// Retained file count limit
/// </summary>
public int RetainedFileCountLimit { get; set; } = 31;
/// <summary>
/// Seq server URL (optional)
/// </summary>
public string? SeqUrl { get; set; }
/// <summary>
/// Elasticsearch URL (optional)
/// </summary>
public string? ElasticsearchUrl { get; set; }
}

View File

@@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using MyNewProjectName.Domain.Entities;
namespace MyNewProjectName.Infrastructure.Persistence.Configurations;
/// <summary>
/// Entity configuration for SampleEntity
/// </summary>
public class SampleEntityConfiguration : IEntityTypeConfiguration<SampleEntity>
{
public void Configure(EntityTypeBuilder<SampleEntity> builder)
{
builder.ToTable("Samples");
builder.HasKey(e => e.Id);
builder.Property(e => e.Name)
.IsRequired()
.HasMaxLength(200);
builder.Property(e => e.Description)
.HasMaxLength(1000);
builder.Property(e => e.CreatedBy)
.HasMaxLength(100);
builder.Property(e => e.UpdatedBy)
.HasMaxLength(100);
// Index for common queries
builder.HasIndex(e => e.Name);
builder.HasIndex(e => e.IsActive);
}
}

View File

@@ -0,0 +1,45 @@
using Microsoft.EntityFrameworkCore;
using MyNewProjectName.Domain.Common;
using MyNewProjectName.Domain.Entities;
using System.Reflection;
namespace MyNewProjectName.Infrastructure.Persistence.Context;
/// <summary>
/// Application database context
/// </summary>
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<SampleEntity> Samples => Set<SampleEntity>();
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
foreach (var entry in ChangeTracker.Entries<AuditableEntity>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedAt = DateTime.UtcNow;
break;
case EntityState.Modified:
entry.Entity.UpdatedAt = DateTime.UtcNow;
break;
}
}
return await base.SaveChangesAsync(cancellationToken);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
base.OnModelCreating(modelBuilder);
}
}

View File

@@ -0,0 +1,90 @@
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using MyNewProjectName.Domain.Common;
using MyNewProjectName.Domain.Interfaces;
using MyNewProjectName.Infrastructure.Persistence.Context;
namespace MyNewProjectName.Infrastructure.Persistence.Repositories;
/// <summary>
/// Generic repository implementation
/// </summary>
public class Repository<T> : IRepository<T> where T : BaseEntity
{
protected readonly ApplicationDbContext _context;
protected readonly DbSet<T> _dbSet;
public Repository(ApplicationDbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public virtual async Task<T?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
return await _dbSet.FindAsync(new object[] { id }, cancellationToken);
}
public virtual async Task<IEnumerable<T>> GetAllAsync(CancellationToken cancellationToken = default)
{
return await _dbSet.ToListAsync(cancellationToken);
}
public virtual async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate, CancellationToken cancellationToken = default)
{
return await _dbSet.Where(predicate).ToListAsync(cancellationToken);
}
public virtual async Task<T?> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate, CancellationToken cancellationToken = default)
{
return await _dbSet.FirstOrDefaultAsync(predicate, cancellationToken);
}
public virtual async Task<bool> AnyAsync(Expression<Func<T, bool>> predicate, CancellationToken cancellationToken = default)
{
return await _dbSet.AnyAsync(predicate, cancellationToken);
}
public virtual async Task<int> CountAsync(Expression<Func<T, bool>>? predicate = null, CancellationToken cancellationToken = default)
{
if (predicate == null)
return await _dbSet.CountAsync(cancellationToken);
return await _dbSet.CountAsync(predicate, cancellationToken);
}
public virtual async Task AddAsync(T entity, CancellationToken cancellationToken = default)
{
await _dbSet.AddAsync(entity, cancellationToken);
}
public virtual async Task AddRangeAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default)
{
await _dbSet.AddRangeAsync(entities, cancellationToken);
}
public virtual void Update(T entity)
{
_dbSet.Attach(entity);
_context.Entry(entity).State = EntityState.Modified;
}
public virtual void UpdateRange(IEnumerable<T> entities)
{
_dbSet.AttachRange(entities);
foreach (var entity in entities)
{
_context.Entry(entity).State = EntityState.Modified;
}
}
public virtual void Remove(T entity)
{
_dbSet.Remove(entity);
}
public virtual void RemoveRange(IEnumerable<T> entities)
{
_dbSet.RemoveRange(entities);
}
}

View File

@@ -0,0 +1,71 @@
using Microsoft.EntityFrameworkCore.Storage;
using MyNewProjectName.Domain.Interfaces;
using MyNewProjectName.Infrastructure.Persistence.Context;
namespace MyNewProjectName.Infrastructure.Persistence.Repositories;
/// <summary>
/// Unit of Work implementation
/// </summary>
public class UnitOfWork : IUnitOfWork
{
private readonly ApplicationDbContext _context;
private IDbContextTransaction? _transaction;
public UnitOfWork(ApplicationDbContext context)
{
_context = context;
}
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
return await _context.SaveChangesAsync(cancellationToken);
}
public async Task BeginTransactionAsync(CancellationToken cancellationToken = default)
{
_transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
}
public async Task CommitTransactionAsync(CancellationToken cancellationToken = default)
{
try
{
await _context.SaveChangesAsync(cancellationToken);
if (_transaction != null)
{
await _transaction.CommitAsync(cancellationToken);
}
}
catch
{
await RollbackTransactionAsync(cancellationToken);
throw;
}
finally
{
if (_transaction != null)
{
await _transaction.DisposeAsync();
_transaction = null;
}
}
}
public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default)
{
if (_transaction != null)
{
await _transaction.RollbackAsync(cancellationToken);
await _transaction.DisposeAsync();
_transaction = null;
}
}
public void Dispose()
{
_transaction?.Dispose();
_context.Dispose();
}
}

View File

@@ -0,0 +1,69 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using MyNewProjectName.Application.Interfaces;
namespace MyNewProjectName.Infrastructure.Services;
/// <summary>
/// Implementation of ICurrentUserService
/// Automatically extracts user information from HttpContext using IHttpContextAccessor
/// This follows Clean Architecture principles - no dependency on concrete middleware
/// </summary>
public class CurrentUserService : ICurrentUserService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CurrentUserService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string? UserId
{
get
{
var user = _httpContextAccessor.HttpContext?.User;
if (user?.Identity?.IsAuthenticated != true)
return null;
return user.FindFirstValue(ClaimTypes.NameIdentifier)
?? user.FindFirstValue("sub")
?? user.FindFirstValue("userId");
}
}
public string? UserName
{
get
{
var user = _httpContextAccessor.HttpContext?.User;
if (user?.Identity?.IsAuthenticated != true)
return null;
return user.FindFirstValue(ClaimTypes.Name)
?? user.FindFirstValue("name")
?? user.FindFirstValue("username");
}
}
public bool? IsAuthenticated
{
get
{
return _httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated;
}
}
public string? Role
{
get
{
var user = _httpContextAccessor.HttpContext?.User;
if (user?.Identity?.IsAuthenticated != true)
return null;
return user.FindFirstValue(ClaimTypes.Role)
?? user.FindFirstValue("role");
}
}
}

View File

@@ -0,0 +1,12 @@
using MyNewProjectName.Application.Interfaces;
namespace MyNewProjectName.Infrastructure.Services;
/// <summary>
/// DateTime service implementation
/// </summary>
public class DateTimeService : IDateTimeService
{
public DateTime Now => DateTime.Now;
public DateTime UtcNow => DateTime.UtcNow;
}