Add project files.

This commit is contained in:
2025-06-29 08:47:52 +02:00
commit 88161dc2ab
46 changed files with 1894 additions and 0 deletions

View File

@ -0,0 +1,11 @@

namespace OfflineDemoApi.Data
{
public interface ISqlDataAccess
{
Task<List<T>> LoadData<T, U>(string storedProcedure, U parameters, string connectionStringName);
Task SaveData<T>(string storedProcedure, T parameters, string connectionStringName);
Task<T> SaveDataScalar<T, U>(string storedProcedure, U parameters, string connectionStringName);
}
}

View File

@ -0,0 +1,58 @@
using Dapper;
using Microsoft.Data.SqlClient;
using System.Data;
namespace OfflineDemoApi.Data;
public class SqlDataAccess : ISqlDataAccess
{
private readonly IConfiguration _config;
public SqlDataAccess(IConfiguration config)
{
_config = config;
}
public async Task<List<T>> LoadData<T, U>(string storedProcedure,
U parameters,
string connectionStringName)
{
string connectionString = _config.GetConnectionString(connectionStringName) ??
throw new KeyNotFoundException("Did not find the connection string specified");
using IDbConnection connection = new SqlConnection(connectionString);
List<T> output = (await connection.QueryAsync<T>(
storedProcedure,
parameters,
commandType: CommandType.StoredProcedure)).ToList();
return output;
}
public async Task<T> SaveDataScalar<T, U>(string storedProcedure,
U parameters,
string connectionStringName)
{
string connectionString = _config.GetConnectionString(connectionStringName) ??
throw new KeyNotFoundException("Did not find the connection string specified");
using IDbConnection connection = new SqlConnection(connectionString);
T? output = await connection.ExecuteScalarAsync<T>(storedProcedure, parameters, commandType: CommandType.StoredProcedure);
return output ?? throw new ArgumentNullException("The return value was null, which is an invalid result.");
}
public async Task SaveData<T>(string storedProcedure,
T parameters,
string connectionStringName)
{
string connectionString = _config.GetConnectionString(connectionStringName) ??
throw new KeyNotFoundException("Did not find the connection string specified");
using IDbConnection connection = new SqlConnection(connectionString);
await connection.ExecuteAsync(storedProcedure, parameters, commandType: CommandType.StoredProcedure);
}
}

View File

@ -0,0 +1,7 @@
namespace OfflineDemoApi.Models;
public class AzureStorageConfig
{
public string? ConnectionString { get; set; }
public string? ContainerName { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace OfflineDemoApi.Models;
public class ShortsModel
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Hashtags { get; set; }
public string Mp4FileUrl { get; set; }
public string ImageFileUrl { get; set; }
public bool IsUploaded { get; set; } = false;
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>9343a209-53dc-41cc-b263-bce9f7c1f999</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Storage.Blobs" Version="12.23.0" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
</ItemGroup>
</Project>

145
OfflineDemoApi/Program.cs Normal file
View File

@ -0,0 +1,145 @@
using Azure.Storage.Blobs;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using OfflineDemoApi.Data;
using OfflineDemoApi.Models;
using System;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
// Bind Azure Storage configuration
var azureStorageConfig = builder.Configuration.GetSection("AzureStorage").Get<AzureStorageConfig>();
// Ensure the blob container exists
var blobServiceClient = new BlobServiceClient(azureStorageConfig.ConnectionString);
var blobContainerClient = blobServiceClient.GetBlobContainerClient(azureStorageConfig.ContainerName);
await blobContainerClient.CreateIfNotExistsAsync();
await blobContainerClient.SetAccessPolicyAsync(Azure.Storage.Blobs.Models.PublicAccessType.None);
builder.Services.AddSingleton(blobContainerClient);
// Configure Kestrel limits
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 524288000*2; // Example: 500 MB
options.Limits.MaxRequestBufferSize = 524288000 * 2; // Example: 100 MB
});
builder.Services.AddSingleton<ISqlDataAccess, SqlDataAccess>();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAnyOrigin", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
var app = builder.Build();
app.UseCors("AllowAnyOrigin");
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.MapGet("/", () => {
return "Hello World";
});
app.MapGet("/api/shorts", async (ISqlDataAccess sql) =>
{
return await sql.LoadData<ShortsModel, dynamic>("spShorts_GetAll", new { }, "sql");
});
app.MapGet("/api/file", async (IConfiguration config, string url) =>
{
var blobUri = new Uri(url);
var storageAccountKey = config.GetValue<string>("AzureStorage:StorageAccountKey");
var storageAccountName = config.GetValue<string>("AzureStorage:StorageAccountName");
var credential = new Azure.Storage.StorageSharedKeyCredential(storageAccountName, storageAccountKey);
var blobClient = new BlobClient(blobUri, credential);
var downloadResponse = await blobClient.DownloadStreamingAsync();
return Results.File(
downloadResponse.Value.Content,
downloadResponse.Value.Details.ContentType,
fileDownloadName: blobUri.Segments.Last()
);
});
app.MapPost("/api/upload", async (HttpRequest request,
BlobContainerClient containerClient,
ISqlDataAccess sql) =>
{
if (!request.HasFormContentType || request.Form.Files.Count == 0)
{
return Results.BadRequest("Invalid form submission. Files are required.");
}
var form = await request.ReadFormAsync();
// Extract form data
var title = form["Title"].ToString();
var description = form["Description"].ToString();
var hashtags = form["Hashtags"].ToString();
var mp4File = form.Files["Mp4File"];
var imageFile = form.Files["ImageFile"];
if (mp4File == null || imageFile == null)
{
return Results.BadRequest("Both MP4 and image files are required.");
}
// Validate file size
const long maxFileSize = 800L * 1024 * 1024; // 800MB
if (mp4File.Length > maxFileSize)
{
return Results.BadRequest("MP4 file exceeds the 800MB limit.");
}
var createNewParameters = new { title, description, hashtags };
int id = await sql.SaveDataScalar<int, dynamic>("spShorts_CreateNew", createNewParameters, "sql");
// Rename files
string? mp4FileName = $"{id}.mp4";
// TODO - Fix this extension lookup
string? imageFileName = $"{id}.{imageFile.FileName.Split('.')[1]}";
// Upload MP4 file
var mp4BlobClient = containerClient.GetBlobClient($"shorts/{mp4FileName}");
await using (var mp4Stream = mp4File.OpenReadStream())
{
await mp4BlobClient.UploadAsync(mp4Stream, true);
}
// Upload Image file
var imageBlobClient = containerClient.GetBlobClient($"images/{imageFileName}");
await using (var imageStream = imageFile.OpenReadStream())
{
await imageBlobClient.UploadAsync(imageStream, true);
}
// Update SQL with the uploaded files
string? mp4FileUrl = mp4BlobClient.Uri.ToString();
string? imageFileUrl = imageBlobClient.Uri.ToString();
var addUploadedFilesParameters = new { id, mp4FileUrl, imageFileUrl };
await sql.SaveData("spShorts_AddUploadedFiles", addUploadedFilesParameters, "sql");
return Results.Ok(new { Message = "Files uploaded to Azure Blob Storage successfully." });
});
app.Run();

View File

@ -0,0 +1,173 @@
{
"$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"_dependencyType": "compute.function.linux.appService"
},
"parameters": {
"resourceGroupName": {
"type": "string",
"defaultValue": "offline-demo-sql",
"metadata": {
"description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking."
}
},
"resourceGroupLocation": {
"type": "string",
"defaultValue": "southcentralus",
"metadata": {
"description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support."
}
},
"resourceName": {
"type": "string",
"defaultValue": "offlinedemoapi",
"metadata": {
"description": "Name of the main resource to be created by this template."
}
},
"resourceLocation": {
"type": "string",
"defaultValue": "[parameters('resourceGroupLocation')]",
"metadata": {
"description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there."
}
}
},
"resources": [
{
"type": "Microsoft.Resources/resourceGroups",
"name": "[parameters('resourceGroupName')]",
"location": "[parameters('resourceGroupLocation')]",
"apiVersion": "2019-10-01"
},
{
"type": "Microsoft.Resources/deployments",
"name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
"resourceGroup": "[parameters('resourceGroupName')]",
"apiVersion": "2019-10-01",
"dependsOn": [
"[parameters('resourceGroupName')]"
],
"properties": {
"mode": "Incremental",
"expressionEvaluationOptions": {
"scope": "inner"
},
"parameters": {
"resourceGroupName": {
"value": "[parameters('resourceGroupName')]"
},
"resourceGroupLocation": {
"value": "[parameters('resourceGroupLocation')]"
},
"resourceName": {
"value": "[parameters('resourceName')]"
},
"resourceLocation": {
"value": "[parameters('resourceLocation')]"
}
},
"template": {
"$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"resourceGroupName": {
"type": "string"
},
"resourceGroupLocation": {
"type": "string"
},
"resourceName": {
"type": "string"
},
"resourceLocation": {
"type": "string"
}
},
"variables": {
"storage_name": "[toLower(concat('storage', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId))))]",
"appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
"storage_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Storage/storageAccounts/', variables('storage_name'))]",
"appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]",
"function_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/sites/', parameters('resourceName'))]"
},
"resources": [
{
"location": "[parameters('resourceLocation')]",
"name": "[parameters('resourceName')]",
"type": "Microsoft.Web/sites",
"apiVersion": "2015-08-01",
"tags": {
"[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty"
},
"dependsOn": [
"[variables('appServicePlan_ResourceId')]",
"[variables('storage_ResourceId')]"
],
"kind": "functionapp",
"properties": {
"name": "[parameters('resourceName')]",
"kind": "functionapp",
"httpsOnly": true,
"reserved": false,
"serverFarmId": "[variables('appServicePlan_ResourceId')]",
"siteConfig": {
"alwaysOn": true,
"linuxFxVersion": "dotnet|3.1"
}
},
"identity": {
"type": "SystemAssigned"
},
"resources": [
{
"name": "appsettings",
"type": "config",
"apiVersion": "2015-08-01",
"dependsOn": [
"[variables('function_ResourceId')]"
],
"properties": {
"AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storage_name'), ';AccountKey=', listKeys(variables('storage_ResourceId'), '2017-10-01').keys[0].value, ';EndpointSuffix=', 'core.windows.net')]",
"FUNCTIONS_EXTENSION_VERSION": "~3",
"FUNCTIONS_WORKER_RUNTIME": "dotnet"
}
}
]
},
{
"location": "[parameters('resourceGroupLocation')]",
"name": "[variables('storage_name')]",
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2017-10-01",
"tags": {
"[concat('hidden-related:', concat('/providers/Microsoft.Web/sites/', parameters('resourceName')))]": "empty"
},
"properties": {
"supportsHttpsTrafficOnly": true
},
"sku": {
"name": "Standard_LRS"
},
"kind": "Storage"
},
{
"location": "[parameters('resourceGroupLocation')]",
"name": "[variables('appServicePlan_name')]",
"type": "Microsoft.Web/serverFarms",
"apiVersion": "2015-02-01",
"kind": "linux",
"properties": {
"name": "[variables('appServicePlan_name')]",
"sku": "Standard",
"workerSizeId": "0",
"reserved": true
}
}
]
}
}
}
]
}

View File

@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5057",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7099;http://localhost:5057",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,18 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"AzureStorage": {
"ConnectionString": "",
"ContainerName": "shorts",
"StorageAccountName": "",
"StorageAccountKey": ""
},
"ConnectionStrings": {
"sql": ""
}
}