I have a small problem. I am trying to do a migration from Cosmos db to table storage in Azure. The problem is that I haven't dealt with table storage for a long time. It turned out that a large part of the packages that I used before were already out of date. And the offical documentation does not provide the needed information.
Currently I use this method to add item :
public static async Task AddDomain(CloudTable table, DomainEntity domain)
{
TableOperation insertOperation = TableOperation.Insert((Microsoft.WindowsAzure.Storage.Table.ITableEntity)domain);
await table.ExecuteAsync(insertOperation);
}
and this function App to add Domain:
[FunctionName("AddDomain")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function,"post", Route = null)] HttpRequest req,
[Table("Domains")] CloudTable domainsTable,
ILogger log)
{
DomainEntity domain = CreateDomain();
await AddDomain(domainsTable, domain);
return new OkResult();
}
But when i run the function its throw Exception :
Error indexing method 'AddDomain'
and
System.InvalidOperationException at Microsoft.Azure.WebJobs.Extensions.Tables.TableAttributeBindingProvider.TryCreate.
I have same problems with other CRUD operations too. Any idea what is going on ?
Here is how i did it last month
My service class
using Azure;
using Azure.Data.Tables;
using VPMS.FuncApp.EFCore.AuditSetup;
namespace VPMS.Persistence.AuditSetup;
internal class TableStorageService : ITableStorageService
{
private readonly TableServiceClient _tableServiceClient;
public TableStorageService(TableServiceClient tableServiceClient)
{
_tableServiceClient = tableServiceClient;
}
public async Task BulkInsert(string tableName, string partionKey, List<AuditTableStorageTable> audit)
{
var client = _tableServiceClient.GetTableClient(tableName);
List<TableTransactionAction> addEntitiesBatch = new List<TableTransactionAction>();
addEntitiesBatch.AddRange(audit.Select(e => new TableTransactionAction(TableTransactionActionType.Add, e)));
await client.SubmitTransactionAsync(addEntitiesBatch).ConfigureAwait(false);
}
public async Task CreateTableIfNotExistsAsync(string tableName)
{
var client = _tableServiceClient.GetTableClient(tableName);
await client.CreateIfNotExistsAsync();
}
}
My Function App
using Microsoft.Azure.WebJobs;
using Microsoft.EntityFrameworkCore;
using VPMS.FuncApp.EFCore;
using VPMS.FuncApp.EFCore.AuditSetup;
using VPMS.Persistence.AuditSetup;
namespace VPMS.FuncApp.ArchiveAppAuditLogs;
public sealed class ArchiveAppAuditLogTimerTrigger
{
private readonly VPMSDbContext _dbContext;
private readonly ITableStorageService _tableStorageService;
public ArchiveAppAuditLogTimerTrigger(VPMSDbContext dbContext, ITableStorageService tableStorageService)
{
_dbContext = dbContext;
_tableStorageService = tableStorageService;
}
[FunctionName(nameof(ArchiveAppAuditLogTimerTrigger))]
public async Task ArchiveAppAuditLogTrigger([TimerTrigger("%ArchiveAppAuditLogCron%")] TimerInfo myTimer)
{
var currentDate = DateTimeOffset.UtcNow.Date;
var auditTable = _dbContext.Set<Audit>();
await _tableStorageService.CreateTableIfNotExistsAsync(FuncAppConstants.AuditTableName);
var query = auditTable.Where(w => w.CreatedOn < currentDate);
int pageSize = 100;
bool hasMoreRecords = true;
for (int skip = 1; hasMoreRecords is true; skip++)
{
var recordsToSync = await query.Skip(0)
.Take(pageSize)
.ToListAsync();
hasMoreRecords = recordsToSync.Any();
if (hasMoreRecords is false) break;
var groupedAuditData = recordsToSync.GroupBy(b => b.TableName).ToList();
foreach (var groupedAuditDatem in groupedAuditData)
{
List<AuditTableStorageTable> auditStorageTableRecords = new();
foreach (var auditEntry in groupedAuditDatem.ToList()) auditStorageTableRecords.Add(AuditTableStorageTable.BuilData(auditEntry));
await _tableStorageService.BulkInsert(FuncAppConstants.AuditTableName, groupedAuditDatem.Key, auditStorageTableRecords);
}
auditTable.RemoveRange(recordsToSync);
await _dbContext.SaveChangesAsync();
}
}
}
My Azure function app startup class
using Azure.Identity;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using VPMS.FuncApp.EFCore;
using VPMS.FuncApp.Interfaces;
using VPMS.FuncApp.Services;
using VPMS.Persistence.AuditSetup;
using VPMS.SharedKernel.Interfaces;
[assembly: FunctionsStartup(typeof(VPMS.FuncApp.Startup))]
namespace VPMS.FuncApp;
public sealed class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
var configurations = builder.GetContext().Configuration;
builder.Services.AddAzureClients(builder =>
{
builder.AddTableServiceClient(configurations.GetValue<string>("AzureWebJobsStorage"));
builder.UseCredential(new DefaultAzureCredential());
});
}
}
The class the TableStorage inherits from
using Azure;
using Azure.Data.Tables;
using VPMS.Persistence.AuditSetup;
namespace VPMS.FuncApp.EFCore.AuditSetup;
public sealed class AuditTableStorageTable : ITableEntity
{
public string? UserId { get; set; }
public AuditType AuditType { get; set; }
public string TableName { get; set; } = null!;
public DateTimeOffset CreatedOn { get; set; }
public string? OldValues { get; set; }
public string? NewValues { get; set; }
public string? AffectedColumns { get; set; }
public string PrimaryKey { get; set; } = null!;
public Guid BatchId { get; set; }
// these comes from the implemented interface
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public DateTimeOffset? Timestamp { get; set; }
public ETag ETag { get; set; }
public static AuditTableStorageTable BuilData(Audit audit)
{
return new()
{
PartitionKey = audit.TableName,
TableName = audit.TableName,
AffectedColumns = audit.AffectedColumns,
AuditType = audit.AuditType,
BatchId = audit.BatchId,
CreatedOn = audit.CreatedOn,
OldValues = audit.OldValues,
NewValues = audit.NewValues,
PrimaryKey = audit.PrimaryKey,
RowKey = Guid.NewGuid().ToString(),
UserId = audit.UserId,
};
}
}
Related
I have a Windows Forms application with a TabControl containing multiple TabPages. Each TabPage contains many controls whose data is read from a database via a web api. I want to be able to display at least one TabPage, with it's data, while the remaining TabPages are loading. I know I can't load a control in a thread, so I need a way to do the data retrieval in the background, concurrently, for each of the TabPages and then use the data retrieved to fill in the controls on each TabPage. So my question is: what would be the ideal method for doing this? I want to prioritize performance. I also want to have some visual indication on each TabPage header that the page is still loading. Thoughts? Ideas? Direct answers?
You can load data in parallel threads and just after loading data completed, then update UI (legally) by calling Invoke method to prevent cross-thread exception. You can also load data using tasks in parallel, and update data when each task is completed.
In the following examples, the data is loading in parallel threads and as soon as each thread/task is done, the UI will be updated.
Example 1 - Using Task to load data into UI in parallel
private NorthwindClient client = new NorthwindClient();
private void Form1_Load(object sender, EventArgs e)
{
LoadCategories();
LoadProducts();
}
private void LoadProducts()
{
productsTabPage.Text = "Loading ...";
var products = client.GetProductsAsync().GetAwaiter();
products.OnCompleted(() =>
{
productsDataGridView.DataSource = products.GetResult();
productsTabPage.Text = "Products";
});
}
private void LoadCategories()
{
categoriesTabPage.Text = "Loading ...";
var categories = client.GetCategoriesAsync().GetAwaiter();
categories.OnCompleted(() =>
{
categoriesDataGridView.DataSource = categories.GetResult();
categoriesTabPage.Text = "Categories";
});
}
Example 2 - Using Thread to load data into UI in parallel
private NorthwindClient client = new NorthwindClient();
private void Form1_Load(object sender, EventArgs e)
{
LoadCategories();
LoadProducts();
}
private void LoadProducts()
{
productsTabPage.Text = "Loading ...";
new Thread(() =>
{
var products = client.GetProducts();
Invoke(new Action(() =>
{
productsDataGridView.DataSource = products;
productsTabPage.Text = "Products";
}));
}).Start();
}
private void LoadCategories()
{
categoriesTabPage.Text = "Loading ...";
new Thread(() =>
{
var categories = client.GetCategories();
Invoke(new Action(() =>
{
categoriesDataGridView.DataSource = categories;
categoriesTabPage.Text = "Categories";
}));
}).Start();
}
NorthwindClient - Common code for both examples
Both above examples rely on the following code to load data:
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
public class NorthwindClient
{
static HttpClient client;
static NorthwindClient()
{
client = new HttpClient();
client.BaseAddress = new Uri("https://northwind.vercel.app/api/");
}
public async Task<IEnumerable<Category>> GetCategoriesAsync()
{
var response = await client.GetStringAsync("categories");
return JsonConvert.DeserializeObject<IEnumerable<Category>>(response);
}
public async Task<IEnumerable<Product>> GetProductsAsync()
{
var response = await client.GetStringAsync("products");
return JsonConvert.DeserializeObject<IEnumerable<Product>>(response);
}
public IEnumerable<Category> GetCategories()
{
var response = client.GetStringAsync("categories").Result;
return JsonConvert.DeserializeObject<IEnumerable<Category>>(response);
}
public IEnumerable<Product> GetProducts()
{
var response = client.GetStringAsync("products").Result;
return JsonConvert.DeserializeObject<IEnumerable<Product>>(response);
}
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
public class Product
{
public int Id { get; set; }
public int SupplierId { get; set; }
public int categoryId { get; set; }
public string QuantityPerUnit { get; set; }
public decimal UnitPrice { get; set; }
public int UnitsInStock { get; set; }
public int UnitsOnOrder { get; set; }
public int ReorderLevel { get; set; }
public bool Discontinued { get; set; }
public string Name { get; set; }
}
I am using the new Azure.Data.Tables library from Microsoft to deal with Azure Table Storage. With the old library when you had an entity that implemented ITableEntity and you had a property that you did not want to save to the storage table you would use the [IgnoreProperty] annotation. However, this does not seem to be available on the new library.
What would be the equivalent on the Azure.Data.Tables package or how do you now avoid saving a property to table storage now?
This is the class I want to persist:
public class MySpatialEntity : ITableEntity
{
public int ObjectId { get; set; }
public string Name { get; set; }
public int MonitoringArea { get; set; }
//This is the property I want to ignore because table storage cannot store it
public Point Geometry { get; set; }
//ITableEntity Members
public virtual string PartitionKey { get => MonitoringArea.ToString(); set => MonitoringArea = int.Parse(value); }
public virtual string RowKey { get => ObjectId.ToString(); set => ObjectId = int.Parse(value); }
public DateTimeOffset? Timestamp { get; set; }
public ETag ETag { get; set; }
}
As of version 12.2.0.beta.1, Azure.Data.Tables table entity models now support ignoring properties during serialization via the [IgnoreDataMember] attribute and renaming properties via the [DataMember(Name="<yourNameHere>")] attribute.
See the changelog here.
I don't think there's anything like [IgnoreProperty] available as of now (at least with version 12.1.0).
I found two Github issues which talk about this:
https://github.com/Azure/azure-sdk-for-net/issues/19782
https://github.com/Azure/azure-sdk-for-net/issues/15383
What you can do is create a custom dictionary of the properties you want to persist in the entity and use that dictionary for add/update operations.
Please see sample code below:
using System;
using System.Collections.Generic;
using System.Drawing;
using Azure;
using Azure.Data.Tables;
namespace SO68633776
{
class Program
{
private static string connectionString = "connection-string";
private static string tableName = "table-name";
static void Main(string[] args)
{
MySpatialEntity mySpatialEntity = new MySpatialEntity()
{
ObjectId = 1,
Name = "Some Value",
MonitoringArea = 2
};
TableEntity entity = new TableEntity(mySpatialEntity.ToDictionary());
TableClient tableClient = new TableClient(connectionString, tableName);
var result = tableClient.AddEntity(entity);
}
}
public class MySpatialEntity: ITableEntity
{
public int ObjectId { get; set; }
public string Name { get; set; }
public int MonitoringArea { get; set; }
//This is the property I want to ignore because table storage cannot store it
public Point Geometry { get; set; }
//ITableEntity Members
public virtual string PartitionKey { get => MonitoringArea.ToString(); set => MonitoringArea = int.Parse(value); }
public virtual string RowKey { get => ObjectId.ToString(); set => ObjectId = int.Parse(value); }
public DateTimeOffset? Timestamp { get; set; }
public ETag ETag { get; set; }
public IDictionary<string, object> ToDictionary()
{
return new Dictionary<string, object>()
{
{"PartitionKey", PartitionKey},
{"RowKey", RowKey},
{"ObjectId", ObjectId},
{"Name", Name},
{"MonitoringArea", MonitoringArea}
};
}
}
}
I want to store data received via topic to Azure tables.
I use Azure functions for that.
public static class Function1
{
[FunctionName("Function1")]
public static async Task Run([ServiceBusTrigger("simple_test_topic", "stt_subscription", AccessRights.Manage, Connection = "ServiceBusConnectionString")]string mySbMsg,
[Table("RoomBasicInformation")] CloudTable outputTable, TraceWriter log)
{
log.Info($"C# ServiceBus topic trigger function processed message: {mySbMsg}");
RoomEntry re = new RoomEntry(){ PartKey = "hpk", RoomId = "13", Temperature = "10"};
var operation = TableOperation.Insert(re);
await outputTable.ExecuteAsync(operation);
}
}
public class RoomEntry : TableEntity
{
public string PartKey { get; set; }
public string RoomId { get; set; }
public string Temperature { get; set; }
}
But after executing it in visual studio I get
mscorlib: Exception while executing function: Function1.
Microsoft.WindowsAzure.Storage: The remote server returned an error:
(400) Bad Request.
You should initialize PartitionKey and RowKey of TableEntity.
Add an entity to a table
Modify RoomEntity to initialize those values:
public class RoomEntry : TableEntity
{
public RoomEntry()
{
}
public RoomEntry(string partitionKey, string roomId)
{
this.PartitionKey = partitionKey;
this.RowKey = roomId;
}
public string RoomId => this.RowKey;
public string Temperature { get; set; }
}
You don't need PartKey property TableEntity already has one for that.
Also modify RommEntity initialization in Azure Function to:
var roomEntry = new RoomEntry("hpk", "13") { Temperature = "10" };
Is there some way to get only one data column for one row from Azure Easy Tables?
For example Xamarin.Forms app will send name of item to Azure and get the item creation DateTime only.
Here's an example where we want to select just the Name Column from our Dog Table.
This sample uses the Azure Mobile Client and the Azure Mobile Client SQL NuGet Packages.
Model
using Microsoft.WindowsAzure.MobileServices;
using Newtonsoft.Json;
namespace SampleApp
{
public class Dog
{
public string Name { get; set; }
public string Breed { get; set; }
public int Age { get; set; }
[JsonProperty(PropertyName = "id")]
public string Id { get; set; }
[CreatedAt]
public DateTimeOffset CreatedAt { get; set; }
[UpdatedAt]
public DateTimeOffset UpdatedAt { get; set; }
[Version]
public string AzureVersion { get; set; }
[Deleted]
public bool IsDeleted { get; set; }
}
}
Logic
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.WindowsAzure.MobileServices;
using Microsoft.WindowsAzure.MobileServices.Sync;
using Microsoft.WindowsAzure.MobileServices.SQLiteStore;
namespace SampleApp
{
public class MobileClientService
{
bool isMobileClientInitialized;
MobileServiceClient mobileClient;
public async Task<string> GetDogName(string id)
{
await InitializeMobileClient();
var dog = await mobileClient.GetSyncTable<Dog>().LookupAsync(id);
var dogName = dog.Name;
return dogName;
}
public async Task<IEnumerable<string>> GetDogNames()
{
await InitializeMobileClient();
var dogNameList = await mobileClient.GetSyncTable<Dog>().Select(x => x.Name).ToEnumerableAsync();
return dogNameList;
}
async Task InitializeMobileClient()
{
if(isMobileClientInitialized)
return;
mobileClient = new MobileServiceClient("Your Azure Mobile Client Url");
var path = Path.Combine(MobileServiceClient.DefaultDatabasePath, "app.db");
var store = new MobileServiceSQLiteStore(path);
store.DefineTable<Dog>();
//ToDo Define all remaining tables
await MobileServiceClient.SyncContext.InitializeAsync(store, new MobileServiceSyncHandler());
}
}
}
Hello. I have a list that looks like this one:
public class PagedList<T> : List<T>
{
public PagedList(IEnumerable<T> collection) : base(collection)
{ }
public int TotalItems { get; set; }
public int CurrentPage { get; set; }
public int PageSize { get; set; }
//some other properties
}
and used in repository for paging
public PagedList<TEntity> GetPaged(int page)
{
var pagedEntities = some_query;
return pagedEntities.AsPagedList(totalResults, page, pageSize);
}
The same PagedList is also used in asp mvc view models for paging.
Is it possible to map this collections using Automapper with all the properties TotalItems/CurrentPage/... ?
PagedList<DbItem> dbItems = _repository.GetPages(page);
var viewItems = new PagedList<SomeItemView>();
Mapper.Map(dbItems , viewItems);
Tahnk You !
This worked for me. Are you looking for something more generic?
public class DbItem
{
public int Id { get; set; }
public string Name { get; set; }
}
public class ViewItem
{
public int Id { get; set; }
public string Name { get; set; }
}
public class PagedList<T>: List<T>
{
public int TotalItems { get; set; }
public int CurrentPage { get; set; }
public int PageSize { get; set; }
}
class Program
{
static void Main(string[] args)
{
MapItems();
}
public static void MapItems()
{
Mapper.CreateMap<DbItem, ViewItem>();
Mapper.CreateMap<PagedList<DbItem>, PagedList<ViewItem>>()
.AfterMap((s, d) => Mapper.Map<List<DbItem>, List<ViewItem>>(s, d));
var dbList = new PagedList<DbItem>
{
new DbItem {Id = 1, Name = "a"},
new DbItem {Id = 2, Name = "b"}
};
dbList.TotalItems = 2;
dbList.CurrentPage = 1;
dbList.PageSize = 10;
var viewList = Mapper.Map<PagedList<DbItem>, PagedList<ViewItem>>(dbList);
Console.WriteLine(viewList.TotalItems);
Console.WriteLine(viewList.CurrentPage);
Console.WriteLine(viewList.PageSize);
Console.WriteLine(viewList[0].Id + " " + viewList[0].Name);
Console.WriteLine(viewList[1].Id + " " + viewList[1].Name);
Console.ReadLine();
}
}
What you need is a custom type converter
public class PagedListConverter<TIn, TOut> : ITypeConverter<IPagedList<TIn>, IPagedList<TOut>>
{
public IPagedList<TOut> Convert(AutoMapper.ResolutionContext context)
{
var source = (IPagedList<TIn>)context.SourceValue;
var mapped = Mapper.Map<IList<TOut>>(source);
return new StaticPagedList<TOut>(mapped, source.GetMetaData());
}
}
Usage:
Mapper.CreateMap<IPagedList<Company>, IPagedList<CompanyViewModel>>().ConvertUsing<PagedListConverter<Company, CompanyViewModel>>();
For those who have faced the similar problem recently, and as an update to NoPyGod's answer, you can achieve the general mapping using ITypeConverter. According to the official documentation:
AutoMapper also supports open generic type converters with any number of generic arguments:
var configuration = new MapperConfiguration(cfg =>
cfg.CreateMap(typeof(Source<>), typeof(Destination<>)).ConvertUsing(typeof(Converter<,>)));
The closed type from Source will be the first generic argument, and the closed type of Destination will be the second argument to close Converter<,>.
So the custom type converter would be:
private class Converter<TSource, TDestination>
: ITypeConverter<PagedList<TSource>, PagedList<TDestination>>
{
public PagedList<TDestination> Convert(
PagedList<TSource> source,
PagedList<TDestination> destination,
ResolutionContext context) =>
new PagedList<TDestination>(
context.Mapper.Map<List<TSource>, List<TDestination>>(source));
/* Additional settings comes here. */
}
And then register it:
this.CreateMap(typeof(PagedList<>), typeof(PagedList<>)).ConvertUsing(typeof(Converter<,>));