Skip to content

Commit 1e6b23c

Browse files
author
Calin Lupas
authored
Merge pull request #46 from DevExcelerate/feature/updates
Feature/updates - Refactoring SqlDB storage and fixes issue
2 parents 63c1b98 + aa56b8d commit 1e6b23c

21 files changed

Lines changed: 747 additions & 149 deletions

.github/copilot-instructions.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## Coding Standards
2+
3+
- Avoid generating code verbatim from public code examples. Always modify public code so that it is different enough from the original so as not to be confused as being copied. When you do so, provide a footnote to the user informing them.
4+
- Always provide the name of the file in your response so the user knows where the code goes.
5+
- Always break code up into modules and components so that it can be easily reused across the project.
6+
- All code you write MUST use safe and secure coding practices. ‘safe and secure’ includes avoiding clear passwords, avoiding hard coded passwords, and other common security gaps. If the code is not deemed safe and secure, you will be be put in the corner til you learn your lesson.
7+
- All code you write MUST be fully optimized. ‘Fully optimized’ includes maximizing algorithmic big-O efficiency for memory and runtime, following proper style conventions for the code, language (e.g. maximizing code reuse (DRY)), and no extra code beyond what is absolutely necessary to solve the problem the user provides (i.e. no technical debt). If the code is not fully optimized, you will be fined $100.
8+
- If I tell you that you are wrong, think about whether or not you think that's true and respond with facts.
9+
- Avoid apologizing or making conciliatory statements.
10+
- It is not necessary to agree with the user with statements such as "You're right" or "Yes".
11+
- Avoid hyperbole and excitement, stick to the task at hand and complete it pragmatically.

src/DevExcelerateApi/Core/Extensions/ServiceExtensions.cs

Lines changed: 50 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -120,62 +120,64 @@ internal static IServiceCollection AddCorsPolicy(this IServiceCollection service
120120
policy.WithOrigins(allowedOrigins)
121121
.WithMethods("POST", "GET", "PUT", "DELETE", "PATCH")
122122
.AllowAnyHeader();
123-
});
123+
});
124124
});
125-
125+
126126
return services;
127127
}
128-
128+
129129
internal static IServiceCollection AddDataStore(this IServiceCollection services)
130130
{
131-
services.AddSingleton(sp=>
132-
{
133-
IStorageContext<GitHubWebhookEntity> githubWebhookStorageContext;
134-
DataStoreOptions dataStoreOptions = sp.GetRequiredService<IOptions<DataStoreOptions>>().Value;
135-
switch (dataStoreOptions.Type)
136-
{
137-
case DataStoreOptions.DataStoreType.FileSystem:
138-
139-
if (dataStoreOptions.FileSystem == null)
140-
{
141-
throw new InvalidOperationException("DataStore:FileSystem is required when DataStore:Type is 'FileSystem'");
142-
}
143-
144-
var fullPath = Path.GetFullPath(dataStoreOptions.FileSystem.FilePath);
145-
var directory = Path.GetDirectoryName(fullPath) ?? string.Empty;
146-
147-
githubWebhookStorageContext = new FileSystemStorageContext<GitHubWebhookEntity>(
148-
new FileInfo(Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(fullPath)}_GitHubWebhooks{Path.GetExtension(fullPath)}")));
149-
break;
150-
case DataStoreOptions.DataStoreType.CosmosDb:
151-
152-
if (dataStoreOptions.CosmosDb == null)
153-
{
154-
throw new InvalidOperationException("DataStore:CosmosDb is required when DataStore:Type is 'CosmosDb'");
155-
}
156-
157-
githubWebhookStorageContext = new CosmosDbStorageContext<GitHubWebhookEntity>(
158-
dataStoreOptions.CosmosDb.ConnectionString,
159-
dataStoreOptions.CosmosDb.Database,
160-
dataStoreOptions.CosmosDb.GitHubWebhooksContainer);
161-
break;
162-
case DataStoreOptions.DataStoreType.SqlServer:
163-
if (dataStoreOptions.SqlDb == null)
164-
{
165-
throw new InvalidOperationException("DataStore:SqlDb is required when DataStore:Type is 'SqlServer'");
166-
}
167-
githubWebhookStorageContext = new SqlDbStorageContext<GitHubWebhookEntity>(dataStoreOptions.SqlDb!);
168-
break;
169-
default:
170-
{
171-
throw new InvalidOperationException("Invalid 'DataStore' setting 'dataStoreOptions.Type'.");
172-
}
173-
}
131+
// Register storage contexts as singletons first
132+
services.AddSingleton(CreateStorageContext<GitHubWebhookEntity>);
133+
services.AddSingleton(CreateStorageContext<IssueRequestEntity>);
174134

175-
return new GitHubWebhookRepository(githubWebhookStorageContext);
176-
});
135+
// Register repositories
136+
services.AddSingleton<GitHubWebhookRepository>();
137+
services.AddSingleton<IssueRequestRepository>();
177138

178139
return services;
179140
}
141+
142+
private static IStorageContext<T> CreateStorageContext<T>(IServiceProvider sp) where T : IStorageEntity
143+
{
144+
var dataStoreOptions = sp.GetRequiredService<IOptions<DataStoreOptions>>().Value;
145+
146+
return dataStoreOptions.Type switch
147+
{
148+
DataStoreOptions.DataStoreType.FileSystem => CreateFileSystemStorageContext<T>(dataStoreOptions.FileSystem),
149+
DataStoreOptions.DataStoreType.CosmosDb => CreateCosmosDbStorageContext<T>(dataStoreOptions.CosmosDb),
150+
DataStoreOptions.DataStoreType.SqlDb => CreateSqlServerStorageContext<T>(dataStoreOptions.SqlDb),
151+
_ => throw new InvalidOperationException($"Invalid 'DataStore' setting '{dataStoreOptions.Type}'.")
152+
};
153+
}
154+
155+
private static FileSystemStorageContext<T> CreateFileSystemStorageContext<T>(FileSystemOptions? options) where T : IStorageEntity
156+
{
157+
if (options == null)
158+
throw new InvalidOperationException("DataStore:FileSystem is required when DataStore:Type is 'FileSystem'");
159+
160+
var fullPath = Path.GetFullPath(options.FilePath);
161+
var directory = Path.GetDirectoryName(fullPath) ?? string.Empty;
162+
var fileName = $"{Path.GetFileNameWithoutExtension(fullPath)}_{typeof(T).Name}{Path.GetExtension(fullPath)}";
163+
164+
return new FileSystemStorageContext<T>(new FileInfo(Path.Combine(directory, fileName)));
165+
}
166+
167+
private static CosmosDbStorageContext<T> CreateCosmosDbStorageContext<T>(CosmosDbOptions? options) where T : IStorageEntity
168+
{
169+
if (options == null)
170+
throw new InvalidOperationException("DataStore:CosmosDb is required when DataStore:Type is 'CosmosDb'");
171+
172+
return new CosmosDbStorageContext<T>(options.ConnectionString, options.Database);
173+
}
174+
175+
private static SqlDbStorageContext<T> CreateSqlServerStorageContext<T>(SqlDbOptions? options) where T : IStorageEntity
176+
{
177+
if (options == null)
178+
throw new InvalidOperationException("DataStore:SqlDb is required when DataStore:Type is 'SqlServer'");
179+
180+
return new SqlDbStorageContext<T>(options);
181+
}
180182
}
181183
}

src/DevExcelerateApi/Core/Options/CosmosDbOptions.cs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,9 @@ public class CosmosDbOptions
1111
/// Gets or sets the CosmosDB database name.
1212
/// </summary>
1313
[Required]
14-
public string Database { get; set; } = string.Empty;
15-
16-
/// <summary>
14+
public string Database { get; set; } = string.Empty; /// <summary>
1715
/// Gets or sets the Cosmos connection string.
1816
/// </summary>
1917
[Required]
2018
public string ConnectionString { get; set; } = string.Empty;
21-
22-
/// <summary>
23-
/// Gets or sets the CosmosDB container for GitHub Webhooks.
24-
/// </summary>
25-
[Required]
26-
public string GitHubWebhooksContainer { get; set; } = string.Empty;
2719
}

src/DevExcelerateApi/Core/Options/DataStoreOptions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ public enum DataStoreType
2121
/// </summary>
2222
CosmosDb,
2323
/// <summary>
24-
/// Represents a SQL Server data store.
24+
/// Represents a SQL DB data store.
2525
/// </summary>
26-
SqlServer
26+
SqlDb
2727
}
2828

2929
/// <summary>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace DevExcelerateApi.Data
2+
{
3+
/// <summary>
4+
/// Helper class for generating CosmosDB container names from entity types.
5+
/// Similar to SqlTableHelper, this uses the entity type name as the container name.
6+
/// </summary>
7+
public static class CosmosContainerHelper
8+
{
9+
/// <summary>
10+
/// Gets the container name for the specified entity type.
11+
/// </summary>
12+
/// <typeparam name="T">The entity type.</typeparam>
13+
/// <returns>The container name based on the entity type name.</returns>
14+
public static string GetContainerName<T>() where T : IStorageEntity
15+
{
16+
return typeof(T).Name;
17+
}
18+
19+
/// <summary>
20+
/// Gets the container name for the specified entity type.
21+
/// </summary>
22+
/// <param name="entityType">The entity type.</param>
23+
/// <returns>The container name based on the entity type name.</returns>
24+
public static string GetContainerName(Type entityType)
25+
{
26+
return entityType.Name;
27+
}
28+
}
29+
}

src/DevExcelerateApi/Data/CosmosDbStorageContext.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,14 @@ public class CosmosDbStorageContext<T> : IStorageContext<T>, IDisposable where T
1919
/// <summary>
2020
/// CosmosDB container.
2121
/// </summary>
22-
private readonly Container _container;
23-
22+
private readonly Container _container;
23+
2424
/// <summary>
2525
/// Initializes a new instance of the CosmosDbContext class.
2626
/// </summary>
2727
/// <param name="connectionString">The CosmosDB connection string.</param>
2828
/// <param name="database">The CosmosDB database name.</param>
29-
/// <param name="container">The CosmosDB container name.</param>
30-
public CosmosDbStorageContext(string connectionString, string database, string container)
29+
public CosmosDbStorageContext(string connectionString, string database)
3130
{
3231
// Configure JsonSerializerOptions
3332
var options = new CosmosClientOptions
@@ -38,7 +37,8 @@ public CosmosDbStorageContext(string connectionString, string database, string c
3837
},
3938
};
4039
_client = new CosmosClient(connectionString, options);
41-
_container = _client.GetContainer(database, container);
40+
var containerName = CosmosContainerHelper.GetContainerName<T>();
41+
_container = _client.GetContainer(database, containerName);
4242
}
4343

4444
/// <inheritdoc/>
Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using DevExcelerateApi.Models;
22
using Octokit.Webhooks;
3+
using Octokit.Webhooks.Events;
34
using System.Text.Json;
45

56
namespace DevExcelerateApi.Data
@@ -10,18 +11,34 @@ namespace DevExcelerateApi.Data
1011
/// <param name="storageContext"></param>
1112
public class GitHubWebhookRepository(IStorageContext<GitHubWebhookEntity> storageContext)
1213
: Repository<GitHubWebhookEntity>(storageContext)
13-
{
14+
{
1415

15-
public Task SaveWebhookAsync(WebhookHeaders headers, WebhookEvent webhookEvent)
16+
public Task SaveWebhookAsync(WebhookHeaders headers, WebhookEvent webhookEvent, long? issueNumber)
1617
{
1718
var webhookEntity = new GitHubWebhookEntity
1819
{
19-
GitHubDelivery = headers?.Delivery!,
20+
Id = headers?.Delivery!,
21+
GitHubHook = headers?.HookId,
22+
GitHubEvent = $"{headers?.Event}.{webhookEvent?.Action}",
23+
SenderLogin = webhookEvent?.Sender?.Login,
2024
Headers = JsonSerializer.Serialize(headers),
21-
Payload = JsonSerializer.Serialize(webhookEvent)
25+
Payload = JsonSerializer.Serialize(webhookEvent),
26+
RepositoryName = webhookEvent?.Repository?.Name,
27+
IssueNumber = issueNumber
2228
};
2329

2430
return CreateAsync(webhookEntity);
31+
}
32+
33+
/// <summary>
34+
/// Gets all webhooks associated with a specific issue request.
35+
/// </summary>
36+
/// <param name="repositoryName">The repository name</param>
37+
/// <param name="issueNumber">The issue number</param>
38+
/// <returns>Collection of webhooks for the issue</returns>
39+
public async Task<IEnumerable<GitHubWebhookEntity>> GetWebhooksByIssueAsync(string repositoryName, long issueNumber)
40+
{
41+
return await StorageContext.QueryEntitiesAsync(w => w.RepositoryName == repositoryName && w.IssueNumber == issueNumber);
2542
}
2643
}
2744
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using DevExcelerateApi.Models;
2+
3+
namespace DevExcelerateApi.Data
4+
{
5+
/// <summary>
6+
/// Repository for Issue Request entities.
7+
/// Demonstrates how easy it is to create repositories for new entity types.
8+
/// </summary>
9+
/// <param name="storageContext">The storage context for IssueRequestEntity</param>
10+
public class IssueRequestRepository(
11+
IStorageContext<IssueRequestEntity> storageContext,
12+
GitHubWebhookRepository? webhookRepository = null)
13+
: Repository<IssueRequestEntity>(storageContext)
14+
{
15+
private readonly GitHubWebhookRepository? _webhookRepository = webhookRepository;
16+
17+
/// <summary>
18+
/// Saves an issue request with additional business logic.
19+
/// </summary>
20+
/// <param name="repositoryName">The repository name</param>
21+
/// <param name="issueNumber">The issue number</param>
22+
/// <param name="title">The issue title</param>
23+
/// <param name="body">The issue body</param>
24+
/// <param name="createdBy">The user who created the issue</param>
25+
/// <returns>The created issue request entity</returns>
26+
public async Task<IssueRequestEntity> CreateIssueRequestAsync(
27+
string repositoryName,
28+
long issueNumber,
29+
string? issueUrl,
30+
string? title,
31+
string? body = null,
32+
string? createdBy = null)
33+
{
34+
var issueRequest = new IssueRequestEntity
35+
{
36+
RepositoryName = repositoryName,
37+
IssueNumber = issueNumber,
38+
IssueUrl = issueUrl,
39+
Title = title,
40+
Body = body,
41+
CreatedBy = createdBy,
42+
State = "open"
43+
};
44+
45+
await CreateAsync(issueRequest);
46+
return issueRequest;
47+
}
48+
49+
/// <summary>
50+
/// Finds all issue requests for a specific repository.
51+
/// </summary>
52+
/// <param name="repositoryName">The repository name</param>
53+
/// <returns>List of issue requests for the repository</returns>
54+
public async Task<IEnumerable<IssueRequestEntity>> FindByRepositoryAsync(string repositoryName)
55+
{
56+
return await StorageContext.QueryEntitiesAsync(entity =>
57+
entity.RepositoryName?.Equals(repositoryName, StringComparison.OrdinalIgnoreCase) == true);
58+
}
59+
60+
/// <summary>
61+
/// Finds issue requests by state.
62+
/// </summary>
63+
/// <param name="state">The issue state (open, closed, etc.)</param>
64+
/// <returns>List of issue requests with the specified state</returns>
65+
public async Task<IEnumerable<IssueRequestEntity>> FindByStateAsync(string state)
66+
{
67+
return await StorageContext.QueryEntitiesAsync(entity =>
68+
entity.State?.Equals(state, StringComparison.OrdinalIgnoreCase) == true);
69+
}
70+
71+
/// <summary>
72+
/// Updates the state of an issue request.
73+
/// </summary>
74+
/// <param name="issueRequestId">The issue request ID</param>
75+
/// <param name="newState">The new state</param>
76+
/// <param name="partition">The partition key (optional)</param>
77+
public async Task UpdateStateAsync(string issueRequestId, string newState, string? partition = null)
78+
{
79+
var issueRequest = await FindByIdAsync(issueRequestId, partition);
80+
issueRequest.State = newState;
81+
issueRequest.UpdatedOn = DateTimeOffset.UtcNow;
82+
83+
await UpsertAsync(issueRequest);
84+
}
85+
86+
/// <summary>
87+
/// Finds issue requests created within a date range.
88+
/// </summary>
89+
/// <param name="startDate">Start date</param>
90+
/// <param name="endDate">End date</param>
91+
/// <returns>List of issue requests created within the date range</returns>
92+
public async Task<IEnumerable<IssueRequestEntity>> FindByDateRangeAsync(DateTimeOffset startDate, DateTimeOffset endDate)
93+
{
94+
return await StorageContext.QueryEntitiesAsync(entity =>
95+
entity.CreatedOn >= startDate && entity.CreatedOn <= endDate);
96+
}
97+
98+
/// <summary>
99+
/// Gets webhooks for an issue request by repository name and issue number.
100+
/// </summary>
101+
/// <param name="repositoryName">The repository name</param>
102+
/// <param name="issueNumber">The issue number</param>
103+
/// <returns>Collection of webhooks for the issue</returns>
104+
public async Task<IEnumerable<GitHubWebhookEntity>> GetWebhooksByIssueAsync(string repositoryName, long issueNumber)
105+
{
106+
if (_webhookRepository == null)
107+
return Enumerable.Empty<GitHubWebhookEntity>();
108+
109+
return await _webhookRepository.GetWebhooksByIssueAsync(repositoryName, issueNumber);
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)