How do you call a stored procedure in EF Core and return the results using a generic class? [closed] - c#

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 3 days ago.
This post was edited and submitted for review 3 days ago.
Improve this question
How can you call a stored procedure using Entity Framework Core and return the results using a generic class?
In .NET Standard Entity Framework, I'm able to call stored procedures and return generic like so:
public IEnumerable<T> ExecuteStoredProcedure<T>(object[] sqlParameters) where T : IStoredProcedure, new()
{
if (sqlParameters == null) sqlParameters = new object[] { };
return DataContext.Database.SqlQuery<T>((new T()).Query, sqlParameters).ToList();
}
This method is not available in the same fashion in Entity Framework Core anymore...

Assuming you're using Code First and are comfortable with the steps necessary to create the actual stored procedure and have that defined in your database, I will focus my answer on how to call that stored procedure in C# and map the results generically.
First you want to create a model that matches the data you expect to get back from your results.
Here is an example:
public class UserTimesheet : IStoredProcedure
{
public string Query => "[dbo].[GetUserTimesheet] #userId, #month, #year";
public DateTime WorkDate { get; set; }
public Guid ProjectId { get; set; }
public int CategoryId { get; set; }
public string? ProjectName { get; set; }
public decimal? Hours { get; set; }
}
Notice this extends an interface called IStoredProcedure with the Query property. More on that later, but it's there to work by convention.
Here is that interface:
public interface IStoredProcedure
{
string Query { get; }
}
Next you'll want to add a DbSet to your database context.
// Put this in your Database Context
public DbSet<UserTimesheet> UserTimesheets { get; set; } = null!;
Now since this doesn't map to an actual table, you will want to add some code to the OnModelCreating to tell EF how to reference it. Again, I'm working on a convention, in this case I only want to apply this setting to models that implement IStoredProcedure, and we can do that with a little reflection to make life easier.
In this case we're going to say it has no key and treat it like a view. I created an extension method to keep things a little cleaner, you can use it like this:
public static class ModelBuilderExtension
{
public static ModelBuilder ConfigureStoredProcedureDbSets(this ModelBuilder modelBuilder)
{
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (typeof(IStoredProcedure).IsAssignableFrom(entityType.ClrType))
{
modelBuilder.Entity(entityType.ClrType).HasNoKey().ToView(null);
}
}
return modelBuilder;
}
}
// Put this in DatabaseContext class
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ConfigureStoredProcedureDbSets();
}
Next you'll want something to hold your generic code. I use a class called StoredProcedureRepository, but you can call it what you like. Here's the code, including the interface (for Dependency Injection, if you like):
public abstract class Repository
{
protected readonly DatabaseContext _context;
protected Repository(DatabaseContext context)
{
_context = context;
}
}
public interface IStoredProcedureRepository
{
IEnumerable<T> ExecuteStoredProcedure<T>(object[] sqlParameters) where T : class, IStoredProcedure, new();
SqlParameter GetSqlParameter(string name, object value, bool isOutput = false);
}
public class StoredProcedureRepository : Repository, IStoredProcedureRepository
{
#region Properties
private const string SQL_PARAMETER_PREFIX = "#";
#endregion
#region Constructor
public StoredProcedureRepository(DatabaseContext context) : base(context)
{
}
#endregion
#region Shared Public Methods
public IEnumerable<T> ExecuteStoredProcedure<T>(object[] sqlParameters) where T : class, IStoredProcedure, new()
{
return _context.Set<T>().FromSqlRaw<T>((new T()).Query, sqlParameters).ToList();
}
public SqlParameter GetSqlParameter(string name, object value, bool isOutput = false)
{
if (!name.StartsWith(SQL_PARAMETER_PREFIX))
{
name += SQL_PARAMETER_PREFIX;
}
var direction = isOutput ? System.Data.ParameterDirection.Output : System.Data.ParameterDirection.Input;
return new SqlParameter
{
ParameterName = name,
Value = value,
Direction = direction
};
}
#endregion
}
There are a few things to notice here. It's referencing the DbSet generically and using FromRawSql and calling the Query (string) property from the IStoredProcedure implementation of your model. So you'll want to make sure that contains your query to execute the stored procedure, in this example that would be "[dbo].[GetUserTimesheet] #userId, #month, #year"
Now you can call this stored procedure generically.
Here is an example:
var parameters = new object[3];
parameters[0] = GetSqlParameter("#userId", userId);
parameters[1] = GetSqlParameter("#month", month);
parameters[2] = GetSqlParameter("#year", year);
IList<UserTimesheet> queryResults = _storedProcedureRepository.ExecuteStoredProcedure<UserTimesheet>(parameters).ToList();
To add new stored procedures, just create their respective models (being sure to implement IStoredProcedure and define their Query property, then add their DbSet to the database context.
For example:
public class UserProject : IStoredProcedure
{
public string Query => "[dbo].[GetUserProjects] #userId";
public Guid ProjectId { get; set; }
public string ProjectName { get; set; }
}
// add this to the database context
public DbSet<UserProject> UserProjects { get; set; } = null!;
then call it like so:
var parameters = new object[1];
parameters[0] = GetSqlParameter("#userId", userId);
IList<UserProject> queryResults = _storedProcedureRepository.ExecuteStoredProcedure<UserProject>(parameters).ToList();

Related

Related data not saved when added after creation with EF Core 6

I have POCO objects that are exposed through a repository that uses EF Core 6 to access a database. I can persist "parent" objects to the database and related data that is added to the parent object before creating is persisted successfully as well. However, when trying to add children (SingleSimulationResult objects) to a parent object (SingleSimulation objects) after it has been created, the children are not persisted to the database.
Here is the code that tries to add and save children to the parent object.
singleSim.AddResultsToSimulation(allResults);
Console.WriteLine($"# results: {singleSim.Results.Count}"); // # results: 2
await scopedRepository.Save();
var test = await scopedRepository.GetById(singleSim.Id);
Console.WriteLine($"# results test: {test.Results.Count}"); // # results: 0
SingleSimulation class (BaseEntity just defines an Id property):
public class SingleSimulation : BaseEntity, IAggregateRoot
{
public string Name { get; private set; }
public string Description { get; private set; }
public double Capital { get; private set; }
public List<List<double>> Returns { get; private set; }
private readonly List<SingleSimulationStrategy> _strategies = new List<SingleSimulationStrategy>();
public IReadOnlyCollection<SingleSimulationStrategy> Strategies => _strategies.AsReadOnly();
private List<SingleSimulationResult> _results = new List<SingleSimulationResult>();
public IReadOnlyCollection<SingleSimulationResult> Results => _results.AsReadOnly();
public SingleSimulation()
{
}
public SingleSimulation(string name, string description, double capital, List<List<double>> returns, List<SingleSimulationStrategy> strategies)
{
Name = name;
Description = description;
Capital = capital;
Returns = returns;
_strategies = strategies;
}
public void AddResultsToSimulation(List<SingleSimulationResult> results)
{
if (_results is null)
return;
foreach (var result in results)
{
_results.Add(result);
}
}
}
Repository class:
public class SingleSimulationRepository : ISingleSimulationRepository
{
private SimulationDbContext _dbContext;
public SingleSimulationRepository(SimulationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task Add(SingleSimulation entity)
{
await _dbContext.AddAsync(entity);
}
public async Task<SingleSimulation> GetById(int id)
{
return await _dbContext.SingleSimulations.FindAsync(id);
}
...
public async Task Save()
{
await _dbContext.SaveChangesAsync();
}
}
DbContext:
public class SimulationDbContext : DbContext
{
public SimulationDbContext(DbContextOptions<SimulationDbContext> options)
: base(options)
{
}
public DbSet<SingleSimulation> SingleSimulations { get; set; }
public DbSet<SingleSimulationResult> SingleSimulationResults { get; set; }
public DbSet<SingleSimulationStrategy> SingleSimulationStrategies { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Seed data and custom conversion functions
}
}
Here's what I have tried (to no avail):
Using Fluent API to configure One-to-Many relationship for Results (using .HasMany()).
modelBuilder.Entity<SingleSimulation>()
.HasMany(x => x.Results)
.WithOne();
Using AddRange() to add result objects to the DB before adding them to the parent and finally saving to DB (SaveChangesAsync).
Using Attach() to start tracking result objects before adding them to the parent.
Using Include() when loading the parent object from the database before adding children and trying to save them.
It feels like I'm missing something small, but after scouring the docs and other sources I cannot find the problem. What do I need to do to get children added to the parent after the parent has already been created to actually save to the DB?
After debugging by printing the EF Core change tracker's LongView, I noticed that no changes are detected on the object (even if changing a simple string property). It turns out the problem was that the singleSim object I was modifying was returned from a different dbContext than the one used by the scopedRepository.
The model setup wasn't the problem after all. Even without the Fluent API config the setup works as intended (even with the read only collections and private backing fields).

Generic repository with Dapper

I'm trying to build a generic repository with Dapper. However, I have some difficulties to implement the CRUD-operations.
Here is some code from the repository:
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
{
internal IDbConnection Connection
{
get
{
return new SqlConnection(ConfigurationManager.ConnectionStrings["SoundyDB"].ConnectionString);
}
}
public GenericRepository(string tableName)
{
_tableName = tableName;
}
public void Delete(TEntity entity)
{
using (IDbConnection cn = Connection)
{
cn.Open();
cn.Execute("DELETE FROM " + _tableName + " WHERE Id=#ID", new { ID = entity.Id });
}
}
}
As you can see, my delete-method takes a TEntity as parameter which is a parameter of type class.
I call my Delete-method from my UserRepository like this:
public class UserRepository : GenericRepository<User>, IUserRepository
{
private readonly IConnectionFactory _connectionFactory;
public UserRepository(IConnectionFactory connectionFactory) : base("User")
{
_connectionFactory = connectionFactory;
}
public async Task<User> Delete(User model)
{
var result = await Delete(model);
return result;
}
}
The thing is that I can't write entity.Id in my Delete-opration in my generic repository. I get a error. So how can I easily implement CRUD-operations like this?
Here is the error message:
TEntity does not contain a definition of "Id" and no extension method "Id" accepting a argument of type "TEntity" could be found
Define an interface like so.
public interface ITypeWithId {
int Id {get;}
}
And make sure your User type implements that interface.
Now apply it to your class as a generic constraint.
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class, ITypeWithId
If you have types that are stored in the repository but DO Not have an Id property then make your delete type constraint specific to the method and not the class. This will allow you to still use the same repository type even with types that might key on something else like a string or a compound (multi) key.
public void Delete<T>(T entity) where T : class, ITypeWithId
{
using (IDbConnection cn = Connection)
{
cn.Open();
cn.Execute("DELETE FROM " + _tableName + " WHERE Id=#ID", new { ID = entity.Id });
}
}
Please don't do this! Your generic repository adds more confusion than value. It's fragile code (string literals for _tableName, invalid cast errors on the id parameter), and introduces a gaping security hole (sql injection via _tableName). If you've chosen Dapper, it's because you want to be in control of your sql, so it makes no sense to generate the sql you send to Dapper.
you have to define an interface like below
public interface IIdentityEntity
{
public int Id { get; set;}
}
all your entities which want to use the class, must implement the IIdentityEntity.
and the first line should be changed to the following
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class,IIdentityEntity
and what was the problem is that you only described the TEntity as class and class does not have an Id in its description so you have to notify compiler that the Generic type implemented an Interface that holds an Id field inside it
In case it helps, I've just published a library Harbin.DataAccess which implements Generic Repositories (Generic Repository Pattern) using "raw" Dapper, Dapper.FastCRUD, and DapperQueryBuilder:
The Inserts/Updates/Deletes are automatically generated by Dapper FastCRUD (class should be decorated with attributes for keys/autoincrement columns)
Supports FastCRUD bulk update, bulk delete, and async methods.
Repositories can be extended with custom Queries and custom Commands (allows/promotes CQRS separation)
Queries can be defined manually (raw sql) or using Dapper FastCRUD syntax
Dynamic Queries (dynamic number of conditions) can be built using DapperQueryBuilder
There are Read-only Connection Wrappers and Read-only Repositories, so it's easy to use read-replicas (or multiple databases)
Support for ADO.NET transactions
Support for mocking Queries and Commands
Sample Insert/Update/Delete (Generic Repository - this uses Dapper FastCRUD):
var conn = new ReadWriteDbConnection(new System.Data.SqlClient.SqlConnection(connectionString));
// Get a IReadWriteRepository<TEntity> which offers some helpers to Query and Write our table:
var repo = conn.GetReadWriteRepository<ContactType>();
var contactType = repo.QueryAll().First();
// Updating a record
contactType.ModifiedDate = DateTime.Now;
repo.Update(contactType);
// Adding a new record
var newContactType = new ContactType() { Name = "NewType", ModifiedDate = DateTime.Now };
repo.Insert(newContactType);
// FastCRUD will automatically update the auto-generated columns back (identity or guid)
// Deleting a record
repo.Delete(newContactType);
[Table("ContactType", Schema = "Person")]
public class ContactType
{
[Key] // if column is part of primary key
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] // if column is auto-increment
public int ContactTypeId { get; set; }
public DateTime ModifiedDate { get; set; }
public string Name { get; set; }
}
Sample Dynamic Queries:
var conn = new ReadDbConnection(new System.Data.SqlClient.SqlConnection(connectionString));
// Get a IReadRepository<TEntity> which offers some helpers to Query our table:
var repo = conn.GetReadRepository<Person>();
// Custom Query (pure Dapper)
var people = repo.Query("SELECT * FROM Person.Person WHERE PersonType = #personType ", new { personType = "EM" } );
// DapperQueryBuilder allows to dynamically append conditions using string interpolation (but injection-safe)
string type = "EM"; string search = "%Sales%";
var dynamicQuery = repo.QueryBuilder(); // if not specified query is initialized with "SELECT * FROM tablename"
dynamicQuery.Where($"PersonType = {type}");
dynamicQuery.Where($"ModifiedDate >= {DateTime.Now.AddDays(-1)} ");
dynamicQuery.Where($"Name LIKE {search}");
// Result is SELECT * FROM [Person].[Person] WHERE PersonType = #p0 AND ModifiedDate >= #p1 AND Name LIKE #p2
var people = dynamicQuery.Query();
Extending Repositories (adding custom Queries and Commands) using Inheritance:
public class PersonRepository : ReadWriteDbRepository<Person>
{
public PersonRepository(IReadWriteDbConnection db) : base(db)
{
}
public virtual IEnumerable<Person> QueryRecentEmployees()
{
return this.Query("SELECT TOP 10 * FROM [Person].[Person] WHERE [PersonType]='EM' ORDER BY [ModifiedDate] DESC");
}
public virtual void UpdateCustomers()
{
this.Execute("UPDATE [Person].[Person] SET [FirstName]='Rick' WHERE [PersonType]='EM' ");
}
}
public void Sample()
{
// Registers that GetReadWriteRepository<Person>() should return a derived type PersonRepository
ReadWriteDbConnection.RegisterRepositoryType<Person, PersonRepository>();
var conn = new ReadWriteDbConnection(new System.Data.SqlClient.SqlConnection(connectionString));
// we know exactly what subtype to expect, so we can just cast.
var repo = (PersonRepository) conn.GetReadWriteRepository<Person>();
repo.UpdateCustomers();
var recentEmployees = repo.QueryRecentEmployees();
}
Full documentation here.

How to perform strict mapping on Dapper

I am using dapper to map SQL result set directly to my C# object, everything works nicely.
I am using statements like this to do the mapping
var result = connection.Query< MyClass >( "sp_select", );
but this statement doesn't seem to enforce exact mapping between the class fields and the columns returned from the database. Meaning, it won't fail when the field on the POCO doesn't exist on the result set.
I do enjoy the fact that the implementation is loose and doesn't enforce any restriction right of the bat, but is there any feature of dapper that would allow me to demand certain fields from the result set before deeming the mapping successful?
You can also try Dapper-Extensions
Here is an example:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
}
[TestFixture]
public class DapperExtensions
{
private SqlConnection _connection;
[SetUp]
public void Init()
{
_connection = new SqlConnection(#"Data Source=.\sqlexpress;Integrated Security=true; Initial Catalog=mydb");
_connection.Open();
_connection.Execute("create table Person(Id int not null, FirstName varchar(100) not null, LastName varchar(100) not null)");
_connection.Execute("insert into Person(Id, FirstName, LastName) values (1, 'Bill', 'Gates')");
}
[TearDown]
public void Teardown()
{
_connection.Execute("drop table Person");
_connection.Close();
}
[Test]
public void Test()
{
var result = _connection.Get<Person>(1);
}
}
The test will fail due to a missing Address column in the Person table.
You can also ignore columns with Custom Maps:
public class PersonMapper : ClassMapper<Person>
{
public PersonMapper()
{
Map(x => x.Address).Ignore();
AutoMap();
}
}
There is no way for you to enforce this "automagically" with an attribute or a flag. You can follow this open Github issue for more background.
This could be accomplished by you manually by mapping each property yourself in a select clause, although at that point you've lost a lot of the power and ease of use of Dapper.
var result = connection.Query<MyClass>("sp_select")
.Select(x =>
{
// manually map each property and verify
// that the data is returned
});

Best approach for view model creation to handle single entity and list - return IQuerable

Looking for input on the best approach/pattern to meet the following requirement for a view model class:
Converts an IQueryable from a repository select to a IQueryable view model query <-- Works fine
Converts a single instance of a db entity to a view model instance <-- is not working, returns NULL
Both use a single method to map db entity to view model properties to avoid mapping replication
Example of what I am attempting, but it is not working...and seems maybe a bit of a hack:
public class WorkOrderDependencyViewModel : IEntity, IViewModel<WorkOrderDependency, WorkOrderDependencyViewModel>
{
public int Id { get; set; } }
public int WorkOrderHeaderId { get; set; }
public int POHeaderId { get; set; }
public decimal RemainQty { get; set; }
//Re-use this mapping logic for both converting a query and converting a single db entity instance. Used by Kendo Grids
public IQueryable<WorkOrderDependencyViewModel> ConvertClassQueryToViewModelQuery(IQueryable<WorkOrderDependency> entityQuery)
{
var viewModelResultQuery = entityQuery
.Select(x => new WorkOrderDependencyViewModel()
{
Id = x.Id,
WorkOrderHeaderId = x.WorkOrderHeaderId,
POHeaderId = x.PODetail.POHeaderId,
RemainQty = x.PODetail.QtyOrdered - x.PODetail.QtyReceived
}
);
return viewModelResultQuery;
}
//convert single instance of db entity to view model, but use existing mapping logic from above method
public WorkOrderDependencyViewModel ConvertClassToViewModel(WorkOrderDependency entity)
{
var entityList = new List<WorkOrderDependency>();
entityList.Add(entity);
var viewModel = ConvertClassQueryToViewModelQuery(entityList.AsQueryable()).FirstOrDefault() as WorkOrderDependencyViewModel;
return viewModel; <------ viewModel is NULL
}
}
Why is viewModel returning NULL?
This would be a much shorter and easier way to do this if you don't need IQueryable
public WorkOrderDependencyViewModel ConvertClassToViewModel(
WorkOrderDependency entity)
{
return new WorkOrderDependencyViewModel
{
Id = entity.Id,
WorkOrderHeaderId = entity.WorkOrderHeaderId,
POHeaderId = entity.PODetail.POHeaderId,
RemainQty = entity.PODetail.QtyOrdered - entity.PODetail.QtyReceived
};
}
Edit
If you are using this as part of a linq query, maybe you can use Automapper or a func like this
private static readonly Expression<Func<WorkOrderDependency, WorkOrderDependencyViewModel>> AsViewModel =
entity => new WorkOrderDependencyViewModel
{
Id = entity.Id,
WorkOrderHeaderId = entity.WorkOrderHeaderId,
POHeaderId = entity.PODetail.POHeaderId,
RemainQty = entity.PODetail.QtyOrdered - entity.PODetail.QtyReceived
};
You would use it in your query like this
public IQueryable<WorkOrderDependencyViewModel> GetViewModel()
{
return repository.WorkOrderDependencies // change to suit your query needs
.Select(AsViewModel);
}

Adding new members to a DbSet<Class> in ASP.NET MVC 4

Here is my scenario: I have a class called Order, which consists of basic information that should be saved in the database after that an order has been made.
In MyPoject.Infrastructure I use following code:
public class ProductDb : DbContext, IProductDataSource
{
public ProductDb()
: base("DefaultConnection")
{
}
public DbSet<Order> Orders { get; set; }
IQueryable<Order> IProductDataSource.Orders
{
get
{
return Orders;
}
set
{
Orders = (DbSet<Order>)value;
}
}
}
In the Controller, I add this:
private IProductDataSource _db = new ProductDb();
public UserController(IProductDataSource db)
{
_db = db;
}
Later on, in the ActionResult, where I want to add data to the order I use following:
var orders = _db.Orders;
var order = new Order();
//add some data to the order variable
_db.Orders.AsEnumerable().Concat(new[] { order });
_db.Save();
However, this does not appear to work. The problem I face is how it is possible to add new items to Order in the database.
EDIT:
IProductDataSource contains following code
public interface IProductDataSource
{
IQueryable<UserProfile> UserProfiles { get; set; }
IQueryable<Order> Orders { get; set; }
void Save();
void Add();
//void Add();
}
Looking at your code, it appears you are using an Interface to create an IQueryable<Order>, presumably to not expose the rest of your systems to Entity Framework. However, an IQueryable<T> is just a special version of IEnumerable<T>, it still does not have access to Entity Framework features.
If you don't mind exposing your DBset<Order>, it is much easier to work that way since DBSet<T> has full support of Entity Framework behind it and supports add, delete, etc.
However, if you don't want to work with a DBSet<T> for some reason, you will need to create your own Add method which takes in your new Order, creates a temporary DBSet<Order>, adds the Order to the DBSet, then saves the changes. You cannot Add directly to the IQueryable<Order>.
You need to do this:
var orders = _db.Orders;
var order = new Order();
//add some data to the order variable
_db.Orders.Add(order); // order is your new Order
_db.Save();

Categories