first commit
This commit is contained in:
65
MyNewProjectName.Infrastructure/DependencyInjection.cs
Normal file
65
MyNewProjectName.Infrastructure/DependencyInjection.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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>
|
||||
17
MyNewProjectName.Infrastructure/Options/DatabaseOptions.cs
Normal file
17
MyNewProjectName.Infrastructure/Options/DatabaseOptions.cs
Normal 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;
|
||||
}
|
||||
42
MyNewProjectName.Infrastructure/Options/JwtOptions.cs
Normal file
42
MyNewProjectName.Infrastructure/Options/JwtOptions.cs
Normal 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;
|
||||
}
|
||||
24
MyNewProjectName.Infrastructure/Options/RedisOptions.cs
Normal file
24
MyNewProjectName.Infrastructure/Options/RedisOptions.cs
Normal 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;
|
||||
}
|
||||
54
MyNewProjectName.Infrastructure/Options/SerilogOptions.cs
Normal file
54
MyNewProjectName.Infrastructure/Options/SerilogOptions.cs
Normal 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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
12
MyNewProjectName.Infrastructure/Services/DateTimeService.cs
Normal file
12
MyNewProjectName.Infrastructure/Services/DateTimeService.cs
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user