Skip to content

Commit cd87188

Browse files
committed
Merge branch 'main' into jcoitino/query-builder-poc
2 parents 201ea8e + c22519b commit cd87188

File tree

11 files changed

+278
-36
lines changed

11 files changed

+278
-36
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/customize-containers-cw.html
2+
3+
files:
4+
"/opt/aws/amazon-cloudwatch-agent/bin/config.json":
5+
mode: "000600"
6+
owner: root
7+
group: root
8+
content: |
9+
{
10+
"agent":{
11+
"metrics_collection_interval":60,
12+
"logfile":"/opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log",
13+
"run_as_user":"cwagent"
14+
},
15+
"metrics":{
16+
"namespace":"CWAgent/AppBuilderData",
17+
"append_dimensions":{
18+
"InstanceId":"${aws:InstanceId}",
19+
"InstanceType":"${aws:InstanceType}",
20+
"AutoScalingGroupName":"${aws:AutoScalingGroupName}"
21+
},
22+
"aggregation_dimensions":[
23+
[ "AutoScalingGroupName", "InstanceId" ],
24+
[ ]
25+
],
26+
"metrics_collected":{
27+
"cpu":{
28+
"resources":[
29+
"*"
30+
],
31+
"measurement":[
32+
"time_idle",
33+
"time_iowait",
34+
"time_system",
35+
"time_user",
36+
"usage_steal",
37+
"usage_system",
38+
"usage_user",
39+
"usage_iowait"
40+
]
41+
},
42+
"mem":{
43+
"measurement":[
44+
"used_percent",
45+
"total",
46+
"available_percent"
47+
]
48+
}
49+
}
50+
}
51+
}
52+
53+
container_commands:
54+
start_cloudwatch_agent:
55+
command: /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a append-config -m ec2 -s -c file:/opt/aws/amazon-cloudwatch-agent/bin/config.json

NorthwindCRUD/Controllers/EmployeesController.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,22 @@ public ActionResult<EmployeeDto[]> GetAll()
4444
}
4545
}
4646

47+
[HttpGet("GetAllAuthorized")]
48+
[Authorize]
49+
public ActionResult<OrderDto[]> GetAllAuthorized()
50+
{
51+
try
52+
{
53+
var employees = this.employeeService.GetAll();
54+
return Ok(this.mapper.Map<EmployeeDb[], EmployeeDto[]>(employees));
55+
}
56+
catch (Exception error)
57+
{
58+
logger.LogError(error.Message);
59+
return StatusCode(500);
60+
}
61+
}
62+
4763
/// <summary>
4864
/// Fetches all employees or a page of employees based on the provided parameters.
4965
/// </summary>
@@ -123,6 +139,26 @@ public ActionResult<CountResultDto> GetEmployeesCount()
123139
}
124140
}
125141

142+
/// <summary>
143+
/// Retrieves the total number of employees.
144+
/// </summary>
145+
/// <returns>Total count of employees as an integer.</returns>
146+
[HttpGet("GetEmployeesCountAuthorized")]
147+
[Authorize]
148+
public ActionResult<CountResultDto> GetEmployeesCountAuthorized()
149+
{
150+
try
151+
{
152+
var count = employeeService.GetAllAsQueryable().Count();
153+
return new CountResultDto() { Count = count };
154+
}
155+
catch (Exception error)
156+
{
157+
logger.LogError(error.Message);
158+
return StatusCode(500);
159+
}
160+
}
161+
126162
[HttpGet("{id}")]
127163
public ActionResult<EmployeeDto> GetById(int id)
128164
{

NorthwindCRUD/Controllers/ProductsController.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,22 @@ public ActionResult<ProductDto[]> GetAll()
4646
}
4747
}
4848

49+
[HttpGet("GetAllAuthorized")]
50+
[Authorize]
51+
public ActionResult<OrderDto[]> GetAllAuthorized()
52+
{
53+
try
54+
{
55+
var products = this.productService.GetAll();
56+
return Ok(this.mapper.Map<ProductDb[], ProductDto[]>(products));
57+
}
58+
catch (Exception error)
59+
{
60+
logger.LogError(error.Message);
61+
return StatusCode(500);
62+
}
63+
}
64+
4965
/// <summary>
5066
/// Fetches all products or a page of products based on the provided parameters.
5167
/// </summary>
@@ -125,6 +141,25 @@ public ActionResult<CountResultDto> GetProductsCount()
125141
}
126142
}
127143

144+
/// <summary>
145+
/// Retrieves the total number of products.
146+
/// </summary>
147+
/// <returns>Total count of products as an integer.</returns>
148+
[HttpGet("GetProductsCountAuthorized")]
149+
public ActionResult<CountResultDto> GetProductsCountAuthorized()
150+
{
151+
try
152+
{
153+
var count = productService.GetAllAsQueryable().Count();
154+
return new CountResultDto() { Count = count };
155+
}
156+
catch (Exception error)
157+
{
158+
logger.LogError(error.Message);
159+
return StatusCode(500);
160+
}
161+
}
162+
128163
[HttpGet("{id}")]
129164
public ActionResult<ProductDto> GetById(int id)
130165
{
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.Text.RegularExpressions;
2+
3+
namespace NorthwindCRUD.Middlewares
4+
{
5+
public class TenantHeaderValidationMiddleware
6+
{
7+
private const string TenantHeaderKey = "X-Tenant-ID";
8+
9+
private readonly RequestDelegate next;
10+
11+
public TenantHeaderValidationMiddleware(RequestDelegate next)
12+
{
13+
this.next = next;
14+
}
15+
16+
public async Task InvokeAsync(HttpContext context)
17+
{
18+
var tenantHeader = context.Request.Headers[TenantHeaderKey].FirstOrDefault();
19+
20+
if (tenantHeader != null && !IsTenantValid(tenantHeader))
21+
{
22+
context.Response.StatusCode = StatusCodes.Status400BadRequest;
23+
await context.Response.WriteAsync($"Invalid format for Header {TenantHeaderKey}");
24+
return;
25+
}
26+
27+
await next(context);
28+
}
29+
30+
private bool IsTenantValid(string tenantId)
31+
{
32+
return Regex.IsMatch(tenantId, "^[A-Za-z0-9-_]{0,40}$");
33+
}
34+
}
35+
}

NorthwindCRUD/Models/Dtos/AddressDto.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public class AddressDto : IAddress
2121
[StringLength(50, ErrorMessage = "Country cannot exceed 50 characters.")]
2222
public string Country { get; set; }
2323

24-
[RegularExpression(@"^\+?[1-9]\d{1,14}$", ErrorMessage = "Phone number is not valid.")]
24+
[RegularExpression(@"^\+?\(?\d{1,5}\)?[-.\s]?\(?\d{1,5}\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,10}$", ErrorMessage = "Phone number is not valid.")]
2525
public string? Phone { get; set; }
2626
}
2727
}

NorthwindCRUD/Models/Dtos/ShipperDto.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public class ShipperDto : IShipper
1111
[StringLength(100, ErrorMessage = "Company Name cannot exceed 100 characters.")]
1212
public string CompanyName { get; set; }
1313

14-
[RegularExpression(@"^\+?[1-9]\d{1,14}$", ErrorMessage = "Phone number is not valid.")]
14+
[RegularExpression(@"^\+?\(?\d{1,5}\)?[-.\s]?\(?\d{1,5}\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,10}$", ErrorMessage = "Phone number is not valid.")]
1515
public string Phone { get; set; }
1616
}
1717
}

NorthwindCRUD/Models/Dtos/SupplierDto.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ public class SupplierDto : ISupplier
3232
[StringLength(50, ErrorMessage = "Country cannot exceed 50 characters.")]
3333
public string? Country { get; set; }
3434

35-
[RegularExpression(@"^\+?[1-9]\d{1,14}$", ErrorMessage = "Phone number is not valid.")]
35+
[RegularExpression(@"^\+?\(?\d{1,5}\)?[-.\s]?\(?\d{1,5}\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,10}$", ErrorMessage = "Phone number is not valid.")]
3636
public string? Phone { get; set; }
3737

38-
[RegularExpression(@"^\+?[1-9]\d{1,14}$", ErrorMessage = "Fax number is not valid.")]
38+
[RegularExpression(@"^\+?\(?\d{1,5}\)?[-.\s]?\(?\d{1,5}\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,10}$", ErrorMessage = "Fax number is not valid.")]
3939
public string? Fax { get; set; }
4040

4141
[RegularExpression(@"^https?:\/\/[^\s$.?#].[^\s]*$", ErrorMessage = "Home Page URL is not valid.")]

NorthwindCRUD/NorthwindCRUD.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,9 @@
4747
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
4848
</ItemGroup>
4949

50+
<ItemGroup>
51+
<None Include=".ebextensions/**/*">
52+
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
53+
</None>
54+
</ItemGroup>
5055
</Project>

NorthwindCRUD/Program.cs

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
using Newtonsoft.Json.Converters;
1212
using NorthwindCRUD.Filters;
1313
using NorthwindCRUD.Helpers;
14+
using NorthwindCRUD.Middlewares;
15+
using NorthwindCRUD.Providers;
1416
using NorthwindCRUD.Services;
1517
using QueryBuilder;
1618

@@ -78,36 +80,10 @@ public static void Main(string[] args)
7880
});
7981
});
8082

81-
var dbProvider = builder.Configuration.GetConnectionString("Provider");
82-
83-
if (dbProvider == "SQLite")
84-
{
85-
// For SQLite in memory to be shared across multiple EF calls, we need to maintain a separate open connection.
86-
// see post https://stackoverflow.com/questions/56319638/entityframeworkcore-sqlite-in-memory-db-tables-are-not-created
87-
var keepAliveConnection = new SqliteConnection(builder.Configuration.GetConnectionString("SQLiteConnectionString"));
88-
keepAliveConnection.Open();
89-
}
90-
91-
builder.Services.AddDbContext<DataContext>(options =>
83+
builder.Services.AddDbContext<DataContext>((serviceProvider, options) =>
9284
{
93-
if (dbProvider == "SqlServer")
94-
{
95-
options.UseSqlServer(builder.Configuration.GetConnectionString("SqlServerConnectionString"));
96-
}
97-
else if (dbProvider == "InMemory")
98-
{
99-
options.ConfigureWarnings(warnOpts =>
100-
{
101-
// InMemory doesn't support transactions and we're ok with it
102-
warnOpts.Ignore(InMemoryEventId.TransactionIgnoredWarning);
103-
});
104-
105-
options.UseInMemoryDatabase(databaseName: builder.Configuration.GetConnectionString("InMemoryDBConnectionString"));
106-
}
107-
else if (dbProvider == "SQLite")
108-
{
109-
options.UseSqlite(builder.Configuration.GetConnectionString("SQLiteConnectionString"));
110-
}
85+
var configurationProvider = serviceProvider.GetRequiredService<DbContextConfigurationProvider>();
86+
configurationProvider.ConfigureOptions(options);
11187
});
11288

11389
var config = new MapperConfiguration(cfg =>
@@ -139,8 +115,10 @@ public static void Main(string[] args)
139115
});
140116

141117
builder.Services.AddAuthorization();
142-
118+
builder.Services.AddHttpContextAccessor();
119+
builder.Services.AddMemoryCache();
143120
builder.Services.AddScoped<DBSeeder>();
121+
builder.Services.AddScoped<DbContextConfigurationProvider>();
144122
builder.Services.AddTransient<CategoryService>();
145123
builder.Services.AddTransient<CustomerService>();
146124
builder.Services.AddTransient<EmployeeTerritoryService>();
@@ -159,7 +137,7 @@ public static void Main(string[] args)
159137

160138
// Necessary to detect if it's behind a load balancer, for example changing protocol, port or hostname
161139
app.UseForwardedHeaders();
162-
140+
app.UseMiddleware<TenantHeaderValidationMiddleware>();
163141
app.UseHttpsRedirection();
164142
app.UseDefaultFiles();
165143
app.UseStaticFiles();
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System.Globalization;
2+
using Microsoft.Data.Sqlite;
3+
using Microsoft.EntityFrameworkCore;
4+
using Microsoft.Extensions.Caching.Memory;
5+
using NorthwindCRUD.Helpers;
6+
7+
namespace NorthwindCRUD.Providers
8+
{
9+
public class DbContextConfigurationProvider
10+
{
11+
private const string DefaultTenantId = "default-tenant";
12+
private const string TenantHeaderKey = "X-Tenant-ID";
13+
private const string DatabaseConnectionCacheKey = "Data-Connection-{0}";
14+
15+
private readonly IHttpContextAccessor context;
16+
private readonly IMemoryCache memoryCache;
17+
private readonly IConfiguration configuration;
18+
19+
public DbContextConfigurationProvider(IHttpContextAccessor context, IMemoryCache memoryCache, IConfiguration configuration)
20+
{
21+
this.context = context;
22+
this.memoryCache = memoryCache;
23+
this.configuration = configuration;
24+
}
25+
26+
public void ConfigureOptions(DbContextOptionsBuilder options)
27+
{
28+
var dbProvider = configuration.GetConnectionString("Provider");
29+
30+
if (dbProvider == "SqlServer")
31+
{
32+
options.UseSqlServer(configuration.GetConnectionString("SqlServerConnectionString"));
33+
}
34+
else if (dbProvider == "SQLite")
35+
{
36+
var tenantId = GetTenantId();
37+
38+
var cacheKey = string.Format(CultureInfo.InvariantCulture, DatabaseConnectionCacheKey, tenantId);
39+
40+
if (!memoryCache.TryGetValue(cacheKey, out SqliteConnection connection))
41+
{
42+
var connectionString = this.GetSqlLiteConnectionString(tenantId);
43+
connection = new SqliteConnection(connectionString);
44+
memoryCache.Set(cacheKey, connection, GetCacheConnectionEntryOptions());
45+
}
46+
47+
// For SQLite in memory to be shared across multiple EF calls, we need to maintain a separate open connection.
48+
// see post https://stackoverflow.com/questions/56319638/entityframeworkcore-sqlite-in-memory-db-tables-are-not-created
49+
connection.Open();
50+
51+
options.UseSqlite(connection).EnableSensitiveDataLogging();
52+
53+
SeedDb(options);
54+
}
55+
}
56+
57+
private static void SeedDb(DbContextOptionsBuilder optionsBuilder)
58+
{
59+
using var dataContext = new DataContext(optionsBuilder.Options);
60+
DBSeeder.Seed(dataContext);
61+
}
62+
63+
private static void CloseConnection(object key, object value, EvictionReason reason, object state)
64+
{
65+
//Used to clear datasource from memory.
66+
(value as SqliteConnection)?.Close();
67+
}
68+
69+
private MemoryCacheEntryOptions GetCacheConnectionEntryOptions()
70+
{
71+
var defaultAbsoluteCacheExpirationInHours = this.configuration.GetValue<int>("DefaultAbsoluteCacheExpirationInHours");
72+
var cacheEntryOptions = new MemoryCacheEntryOptions
73+
{
74+
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(defaultAbsoluteCacheExpirationInHours),
75+
};
76+
77+
cacheEntryOptions.RegisterPostEvictionCallback(CloseConnection);
78+
79+
return cacheEntryOptions;
80+
}
81+
82+
private string GetSqlLiteConnectionString(string tenantId)
83+
{
84+
var connectionStringTemplate = configuration.GetConnectionString("SQLiteConnectionString");
85+
var unsanitizedConntectionString = string.Format(CultureInfo.InvariantCulture, connectionStringTemplate, tenantId);
86+
var connectionStringBuilder = new SqliteConnectionStringBuilder(unsanitizedConntectionString);
87+
var sanitizedConntectionString = connectionStringBuilder.ToString();
88+
89+
return sanitizedConntectionString;
90+
}
91+
92+
private string GetTenantId()
93+
{
94+
return context.HttpContext?.Request.Headers[TenantHeaderKey].FirstOrDefault() ?? DefaultTenantId;
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)