diff --git a/templates/todo/api/csharp/ListsController.cs b/templates/todo/api/csharp/ListsController.cs new file mode 100644 index 00000000000..97d315c19bb --- /dev/null +++ b/templates/todo/api/csharp/ListsController.cs @@ -0,0 +1,159 @@ +using Microsoft.AspNetCore.Mvc; + +namespace SimpleTodo.Api; + +[ApiController] +[Route("/lists")] +public class ListsController : ControllerBase +{ + private readonly ListsRepository _repository; + + public ListsController(ListsRepository repository) + { + _repository = repository; + } + + [HttpGet] + [ProducesResponseType(200)] + public async Task>> GetLists([FromQuery] int? skip = null, [FromQuery] int? batchSize = null) + { + return Ok(await _repository.GetListsAsync(skip, batchSize)); + } + + [HttpPost] + [ProducesResponseType(201)] + public async Task CreateList([FromBody]CreateUpdateTodoList list) + { + var todoList = new TodoList(list.name) + { + Description = list.description + }; + + await _repository.AddListAsync(todoList); + + return CreatedAtAction(nameof(GetList), new { list_id = todoList.Id }, todoList); + } + + [HttpGet("{list_id}")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public async Task>> GetList(string list_id) + { + var list = await _repository.GetListAsync(list_id); + + return list == null ? NotFound() : Ok(list); + } + + [HttpPut("{list_id}")] + [ProducesResponseType(200)] + public async Task> UpdateList(string list_id, [FromBody]CreateUpdateTodoList list) + { + var existingList = await _repository.GetListAsync(list_id); + if (existingList == null) + { + return NotFound(); + } + + existingList.Name = list.name; + existingList.Description = list.description; + existingList.UpdatedDate = DateTimeOffset.UtcNow; + + await _repository.UpdateList(existingList); + + return Ok(existingList); + } + + [HttpDelete("{list_id}")] + [ProducesResponseType(204)] + public async Task DeleteList(string list_id) + { + if (await _repository.GetListAsync(list_id) == null) + { + return NotFound(); + } + + await _repository.DeleteListAsync(list_id); + + return NoContent(); + } + + [HttpGet("{list_id}/items")] + [ProducesResponseType(200)] + public async Task>> GetListItems(string list_id, [FromQuery] int? skip = null, [FromQuery] int? batchSize = null) + { + return Ok(await _repository.GetListItemsAsync(list_id, skip, batchSize)); + } + + [HttpPost("{list_id}/items")] + [ProducesResponseType(201)] + public async Task> CreateListItem(string list_id, [FromBody] CreateUpdateTodoItem item) + { + var newItem = new TodoItem(list_id, item.name) + { + Name = item.name, + Description = item.description, + State = item.state, + CreatedDate = DateTimeOffset.UtcNow + }; + + await _repository.AddListItemAsync(newItem); + + return CreatedAtAction(nameof(GetListItem), new { list_id = list_id, item_id = newItem.Id }, newItem); + } + + [HttpGet("{list_id}/items/{item_id}")] + [ProducesResponseType(200)] + public async Task> GetListItem(string list_id, string item_id) + { + var item = await _repository.GetListItemAsync(list_id, item_id); + + return item == null ? NotFound() : Ok(item); + } + + [HttpPut("{list_id}/items/{item_id}")] + [ProducesResponseType(200)] + public async Task> UpdateListItem(string list_id, string item_id, [FromBody] CreateUpdateTodoItem item) + { + var existingItem = await _repository.GetListItemAsync(list_id, item_id); + if (existingItem == null) + { + return NotFound(); + } + + existingItem.Name = item.name; + existingItem.Description = item.description; + existingItem.CompletedDate = item.completedDate; + existingItem.DueDate = item.dueDate; + existingItem.State = item.state; + existingItem.UpdatedDate = DateTimeOffset.UtcNow; + + await _repository.UpdateListItem(existingItem); + + return Ok(existingItem); + } + + [HttpDelete("{list_id}/items/{item_id}")] + [ProducesResponseType(204)] + [ProducesResponseType(404)] + public async Task DeleteListItem(string list_id, string item_id) + { + if (await _repository.GetListItemAsync(list_id, item_id) == null) + { + return NotFound(); + } + + await _repository.DeleteListItemAsync(list_id, item_id); + + return NoContent(); + } + + [HttpGet("{list_id}/state/{state}")] + [ProducesResponseType(200)] + public async Task>> GetListItemsByState(string list_id, string state, [FromQuery] int? skip = null, [FromQuery] int? batchSize = null) + { + return Ok(await _repository.GetListItemsByStateAsync(list_id, state, skip, batchSize)); + } + + public record CreateUpdateTodoList(string name, string? description = null); + public record CreateUpdateTodoItem(string name, string state, DateTimeOffset? dueDate, DateTimeOffset? completedDate, string? description = null); +} \ No newline at end of file diff --git a/templates/todo/api/csharp/ListsRepository.cs b/templates/todo/api/csharp/ListsRepository.cs index 05922b191ec..e5bb9f4188c 100644 --- a/templates/todo/api/csharp/ListsRepository.cs +++ b/templates/todo/api/csharp/ListsRepository.cs @@ -1,115 +1,132 @@ -using Microsoft.Azure.Cosmos; -using Microsoft.Azure.Cosmos.Linq; - -namespace SimpleTodo.Api; - -public class ListsRepository -{ - private readonly Container _listsCollection; - private readonly Container _itemsCollection; - - public ListsRepository(CosmosClient client, IConfiguration configuration) - { - var database = client.GetDatabase(configuration["AZURE_COSMOS_DATABASE_NAME"]); - _listsCollection = database.GetContainer("TodoList"); - _itemsCollection = database.GetContainer("TodoItem"); - } - - public async Task> GetListsAsync(int? skip, int? batchSize) - { - return await ToListAsync( - _listsCollection.GetItemLinqQueryable(), - skip, - batchSize); - } - - public async Task GetListAsync(string listId) - { - var response = await _listsCollection.ReadItemAsync(listId, new PartitionKey(listId)); - return response?.Resource; - } - - public async Task DeleteListAsync(string listId) - { - await _listsCollection.DeleteItemAsync(listId, new PartitionKey(listId)); - } - - public async Task AddListAsync(TodoList list) - { - list.Id = Guid.NewGuid().ToString("N"); - await _listsCollection.UpsertItemAsync(list, new PartitionKey(list.Id)); - } - - public async Task UpdateList(TodoList existingList) - { - await _listsCollection.ReplaceItemAsync(existingList, existingList.Id, new PartitionKey(existingList.Id)); - } - - public async Task> GetListItemsAsync(string listId, int? skip, int? batchSize) - { - return await ToListAsync( - _itemsCollection.GetItemLinqQueryable().Where(i => i.ListId == listId), - skip, - batchSize); - } - - public async Task> GetListItemsByStateAsync(string listId, string state, int? skip, int? batchSize) - { - return await ToListAsync( - _itemsCollection.GetItemLinqQueryable().Where(i => i.ListId == listId && i.State == state), - skip, - batchSize); - } - - public async Task AddListItemAsync(TodoItem item) - { - item.Id = Guid.NewGuid().ToString("N"); - await _itemsCollection.UpsertItemAsync(item, new PartitionKey(item.Id)); - } - - public async Task GetListItemAsync(string listId, string itemId) - { - var response = await _itemsCollection.ReadItemAsync(itemId, new PartitionKey(itemId)); - if (response?.Resource.ListId != listId) - { - return null; - } - return response.Resource; - } - - public async Task DeleteListItemAsync(string listId, string itemId) - { - await _itemsCollection.DeleteItemAsync(itemId, new PartitionKey(itemId)); - } - - public async Task UpdateListItem(TodoItem existingItem) - { - await _itemsCollection.ReplaceItemAsync(existingItem, existingItem.Id, new PartitionKey(existingItem.Id)); - } - - private async Task> ToListAsync(IQueryable queryable, int? skip, int? batchSize) - { - if (skip != null) - { - queryable = queryable.Skip(skip.Value); - } - - if (batchSize != null) - { - queryable = queryable.Take(batchSize.Value); - } - - using FeedIterator iterator = queryable.ToFeedIterator(); - var items = new List(); - - while (iterator.HasMoreResults) - { - foreach (var item in await iterator.ReadNextAsync()) - { - items.Add(item); - } - } - - return items; - } +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Conventions; +using MongoDB.Bson.Serialization.IdGenerators; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver; + +namespace SimpleTodo.Api; + +public class ListsRepository +{ + private readonly IMongoCollection _listsCollection; + private readonly IMongoCollection _itemsCollection; + + static ListsRepository() + { + var conventionPack = new ConventionPack + { + new CamelCaseElementNameConvention() + }; + + ConventionRegistry.Register( + name: "Camel case", + conventions: conventionPack, + filter: t => t == typeof(TodoList) || t == typeof(TodoItem)); + + var objectIdSerializer = new StringSerializer(BsonType.ObjectId); + BsonClassMap.RegisterClassMap(map => + { + map.AutoMap(); + map.MapIdProperty(item => item.Id) + .SetIgnoreIfDefault(true) + .SetIdGenerator(StringObjectIdGenerator.Instance) + .SetSerializer(objectIdSerializer); + }); + + BsonClassMap.RegisterClassMap(map => + { + map.AutoMap(); + map.MapIdProperty(item => item.Id) + .SetIgnoreIfDefault(true) + .SetIdGenerator(StringObjectIdGenerator.Instance) + .SetSerializer(objectIdSerializer); + map.MapProperty(item => item.ListId).SetSerializer(objectIdSerializer); + }); + } + + public ListsRepository(MongoClient client, IConfiguration configuration) + { + var database = client.GetDatabase(configuration["AZURE_COSMOS_DATABASE_NAME"]); + _listsCollection = database.GetCollection("TodoList"); + _itemsCollection = database.GetCollection("TodoItem"); + } + + public async Task> GetListsAsync(int? skip, int? batchSize) + { + var cursor = await _listsCollection.FindAsync( + _ => true, + new FindOptions() + { + Skip = skip, + BatchSize = batchSize + }); + return await cursor.ToListAsync(); + } + + public async Task GetListAsync(string listId) + { + var cursor = await _listsCollection.FindAsync(list => list.Id == listId); + return await cursor.FirstOrDefaultAsync(); + } + + public async Task DeleteListAsync(string listId) + { + await _listsCollection.DeleteOneAsync(list => list.Id == listId); + } + + public async Task AddListAsync(TodoList list) + { + await _listsCollection.InsertOneAsync(list); + } + + public async Task UpdateList(TodoList existingList) + { + await _listsCollection.ReplaceOneAsync(list => list.Id == existingList.Id, existingList); + } + + public async Task> GetListItemsAsync(string listId, int? skip, int? batchSize) + { + var cursor = await _itemsCollection.FindAsync( + item => item.ListId == listId, + new FindOptions() + { + Skip = skip, + BatchSize = batchSize + }); + return await cursor.ToListAsync(); + } + + public async Task> GetListItemsByStateAsync(string listId, string state, int? skip, int? batchSize) + { + var cursor = await _itemsCollection.FindAsync( + item => item.ListId == listId && item.State == state, + new FindOptions() + { + Skip = skip, + BatchSize = batchSize + }); + return await cursor.ToListAsync(); + } + + public async Task AddListItemAsync(TodoItem item) + { + await _itemsCollection.InsertOneAsync(item); + } + + public async Task GetListItemAsync(string listId, string itemId) + { + var cursor = await _itemsCollection.FindAsync(item => item.Id == itemId && item.ListId == listId); + return await cursor.FirstOrDefaultAsync(); + } + + public async Task DeleteListItemAsync(string listId, string itemId) + { + await _itemsCollection.DeleteOneAsync(item => item.Id == itemId && item.ListId == listId); + } + + public async Task UpdateListItem(TodoItem existingItem) + { + await _itemsCollection.ReplaceOneAsync(item => item.Id == existingItem.Id, existingItem); + } } \ No newline at end of file diff --git a/templates/todo/api/csharp/Program.cs b/templates/todo/api/csharp/Program.cs index 419e4931d34..ce5f387c836 100644 --- a/templates/todo/api/csharp/Program.cs +++ b/templates/todo/api/csharp/Program.cs @@ -1,5 +1,6 @@ using Azure.Identity; -using Microsoft.Azure.Cosmos; +using Microsoft.ApplicationInsights.AspNetCore.Extensions; +using MongoDB.Driver; using SimpleTodo.Api; var builder = WebApplication.CreateBuilder(args); @@ -7,11 +8,10 @@ builder.Configuration.AddAzureKeyVault(new Uri(builder.Configuration["AZURE_KEY_VAULT_ENDPOINT"]), credential); builder.Services.AddSingleton(); -builder.Services.AddSingleton(_ => new CosmosClient(builder.Configuration[builder.Configuration["AZURE_COSMOS_CONNECTION_STRING_KEY"]])); -builder.Services.AddCors(); +builder.Services.AddSingleton(_ => new MongoClient(builder.Configuration[builder.Configuration["AZURE_COSMOS_CONNECTION_STRING_KEY"]])); +builder.Services.AddControllers(); builder.Services.AddApplicationInsightsTelemetry(builder.Configuration); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); + var app = builder.Build(); app.UseCors(policy => @@ -32,7 +32,5 @@ ServeUnknownFileTypes = true, }); -app.MapGroup("/lists") - .MapTodoApi() - .WithOpenApi(); +app.MapControllers(); app.Run(); \ No newline at end of file diff --git a/templates/todo/api/csharp/Todo.Api.csproj b/templates/todo/api/csharp/Todo.Api.csproj index c71ee2733c0..1e8ec0c3f5e 100644 --- a/templates/todo/api/csharp/Todo.Api.csproj +++ b/templates/todo/api/csharp/Todo.Api.csproj @@ -1,19 +1,16 @@ - - - - net8.0 - enable - enable - false - - - - - - - - - - - - + + + + net6.0 + enable + enable + + + + + + + + + + diff --git a/templates/todo/api/csharp/Todo.Api.sln b/templates/todo/api/csharp/Todo.Api.sln index ec4c80e672d..53504429d22 100644 --- a/templates/todo/api/csharp/Todo.Api.sln +++ b/templates/todo/api/csharp/Todo.Api.sln @@ -1,25 +1,25 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.31903.286 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Todo.Api", "Todo.Api.csproj", "{A40339D0-DEF8-4CF7-9E57-CA227AA78056}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A40339D0-DEF8-4CF7-9E57-CA227AA78056}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A40339D0-DEF8-4CF7-9E57-CA227AA78056}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A40339D0-DEF8-4CF7-9E57-CA227AA78056}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A40339D0-DEF8-4CF7-9E57-CA227AA78056}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {9DE28BD5-F5D0-4A5F-98AC-5404AC5F2FC1} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.31903.286 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Todo.Api", "Todo.Api.csproj", "{A40339D0-DEF8-4CF7-9E57-CA227AA78056}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A40339D0-DEF8-4CF7-9E57-CA227AA78056}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A40339D0-DEF8-4CF7-9E57-CA227AA78056}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A40339D0-DEF8-4CF7-9E57-CA227AA78056}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A40339D0-DEF8-4CF7-9E57-CA227AA78056}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9DE28BD5-F5D0-4A5F-98AC-5404AC5F2FC1} + EndGlobalSection +EndGlobal diff --git a/templates/todo/api/csharp/TodoItem.cs b/templates/todo/api/csharp/TodoItem.cs index da6d3d67f83..4a0073d19a5 100644 --- a/templates/todo/api/csharp/TodoItem.cs +++ b/templates/todo/api/csharp/TodoItem.cs @@ -1,20 +1,20 @@ -namespace SimpleTodo.Api; - -public class TodoItem -{ - public TodoItem(string listId, string name) - { - ListId = listId; - Name = name; - } - - public string? Id { get; set; } - public string ListId { get; set; } - public string Name { get; set; } - public string? Description { get; set; } - public string State { get; set; } = "todo"; - public DateTimeOffset? DueDate { get; set; } - public DateTimeOffset? CompletedDate { get; set; } - public DateTimeOffset? CreatedDate { get; set; } = DateTimeOffset.UtcNow; - public DateTimeOffset? UpdatedDate { get; set; } +namespace SimpleTodo.Api; + +public class TodoItem +{ + public TodoItem(string listId, string name) + { + ListId = listId; + Name = name; + } + + public string? Id { get; set; } + public string ListId { get; set; } + public string Name { get; set; } + public string? Description { get; set; } + public string State { get; set; } = "todo"; + public DateTimeOffset? DueDate { get; set; } + public DateTimeOffset? CompletedDate { get; set; } + public DateTimeOffset? CreatedDate { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset? UpdatedDate { get; set; } } \ No newline at end of file diff --git a/templates/todo/api/csharp/TodoList.cs b/templates/todo/api/csharp/TodoList.cs index 2eb10660d86..74a6c4f2f65 100644 --- a/templates/todo/api/csharp/TodoList.cs +++ b/templates/todo/api/csharp/TodoList.cs @@ -1,15 +1,15 @@ -namespace SimpleTodo.Api; - -public class TodoList -{ - public TodoList(string name) - { - Name = name; - } - - public string? Id { get; set; } - public string Name { get; set; } - public string? Description { get; set; } - public DateTimeOffset CreatedDate { get; set; } = DateTimeOffset.UtcNow; - public DateTimeOffset? UpdatedDate { get; set; } +namespace SimpleTodo.Api; + +public class TodoList +{ + public TodoList(string name) + { + Name = name; + } + + public string? Id { get; set; } + public string Name { get; set; } + public string? Description { get; set; } + public DateTimeOffset CreatedDate { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset? UpdatedDate { get; set; } } \ No newline at end of file